IoT con Vision Google API

Fecha publicación
Dificultad
Intermedio
#
36
3 more properties
Publicado
tags
arduino, esp8266, iot, internet of things, m5stack, M5core, mqtt, mosquitto, vision, ffmpeg, google cloud platform, google cloud vision, OCR, cloud, node-red, nodered, docker, docker-compose


Descripción

Disponemos de este descalcificador instalado en el garaje:
escenari.jpg
Usando una cámara IP vamos a capturar la imagen de lo que sale en la pantalla del descalcificador. Como interface de usuario tendremos un M5Stack basado en ESP32. Así pues, cada vez que pulsemos un botón del dispositivo IoT (M5Stack) este se publicará en un broker de mensajes MQTT. Se capturará el mensaje en NodeRED donde se mandará la imagen a Google Cloud Platform, concretamente al servicio de Vision para que se reconozcan los carácteres de la imagen. Esta información se mandará de vuelta a la pantalla del M5Stack para que la visualice el usuario.

El esquema del escenario quedaría así:
IoT+IA.drawio.png
Para los que no conozcaís M5Stack tiene esta pinta:
Untitled

Configuración de la cámara

Se ha usado una cámara IP que ha costado unos ~25€; un modelo muy sencillo y francamente malo. Tanto es así que cuando sincroniza la hora si le activas el DST (Dailight Saving Time) el reloj deja de funcionar correctamente.
Lo único que nos interesa a nosotros es que dispone de un stream RTSP y a pesar de que la cámara dispone de iluminación LED finalmente la hemos forzado para que trabaje con los LEDs apgados. Ya que con la luz de la pantalla LCD del descalcificador se ve mejor al hacer la captura.
Untitled

Stream de vídeo

Desde la web para configurar la cámara IP podemos ver cuál es el puerto ONVIF (SOAP) que nos va a permitir descubrir la dirección URL del protocolo RTSP para recibir el stream de vídeo:
Untitled

Gracias al programa ONVIF Manager, usamos la IP de nuestra cámara por el puerto indicado en la captura anterior y las credenciales para conectar a través de ONVIF, en nuestro caso:
Bash
http://10.2.8.95:8000
Dentro del ONVIF manager podremos descubrir la dirección RTSP:
Untitled
Podemos probar la dirección RTSP desde VLC, importante mencionar y que no se ve en las capturas. Hay que poner las credenciales en la URL así:
Bash
rtsp://admin:ges2ges.@10.2.8.95/h264/ch1/sub/av_stream
Aquí podemos ver el RTSP mostrando el stream de vídeo:
Untitled

Captura imágenes de un vídeo

Para extraer un frame del vídeo recibido a través de RTSP usaremos FFMPEG. Para ser más concretos, usaremos FFMPEG a través de un contenedor de Docker para no tener que instalarlo en nuestro sistema y para que sea más portable.
El comando que usaremos es el siguiente:
Bash
docker run --rm jrottenberg/ffmpeg -y -rtsp_transport tcp -i 'rtsp://admin:ges2ges.@10.2.8.95/h264/ch1/sub/av_stream' -filter:v "crop=704:536:0:20" -vframes 1 -f image2pipe -vcodec png -
Este comando nos va a devolver por la salida standard output un stream binario en formato PNG con un frame de vídeo. Si queremos guardarlo en un fichero solo tenemos que redirigir la salida hacia un fichero y podremos ver la imagen.
Vamos a explicar los parámetros:

La imagen capturada tiene este aspecto:
image.png

Creación de imagen Docker de NodeRED

Para poder ejecutar código contenedores Docker desde dentro del contenedor de NodeRED nos hemos creado nuestra propia imagen de Docker para correr NodeRED con el servicio Docker corriendo.
Hay que dejar los siguientes tres ficheros en un mismo directorio: Dockerfile, sudoers y docker-compose.yml.
Así podremos crear nuestra imagen de docker simplemente con el comando:
Bash
docker-compose build 

Dockerfile

Bash
FROM nodered/node-red:latest

USER root
# installing docker
RUN apk update
RUN apk add sudo
RUN apk add docker docker-compose
RUN addgroup node-red adm
COPY sudoers /etc/sudoers

sudoers

Bash
root ALL=(ALL:ALL) ALL

%adm   ALL=(ALL:ALL) NOPASSWD:ALL

@includedir /etc/sudoers.d

docker-compose.yml

YAML
version: "3.8"
services:
  node-red:
    container_name: node-red
    image: node-red
    environment:
      - TZ=Europe/Madrid
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    healthcheck:
        test: ["CMD", "curl", "--fail", "http://localhost:1880/", "||","exit 1" ]
        interval: 60s
        timeout: 3s
        retries: 3
        start_period: 30s
    restart: unless-stopped
    network_mode: host

Flow de NodeRED

Untitled

Código del flow

JSON
[{"id":"19ade742bc711664","type":"tab","label":"GCP Vision","disabled":false,"info":""},{"id":"eacb72256f44156c","type":"inject","z":"19ade742bc711664","name":"manual test","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":150,"y":120,"wires":[["a551c355c64013a1"]]},{"id":"a551c355c64013a1","type":"exec","z":"19ade742bc711664","command":"sudo docker run --rm jrottenberg/ffmpeg -y -rtsp_transport tcp -i 'rtsp://admin:ges2ges.@10.2.8.95/h264/ch1/sub/av_stream' -filter:v \"crop=704:536:0:20\" -vframes 1 -f image2pipe -vcodec png -","addpay":"","append":"","useSpawn":"false","timer":"60","oldrc":false,"name":"ffmpeg","x":650,"y":300,"wires":[["3ee69db9.089ae2","2d14dd1f.1b0a22"],["2a2a43a2.4777ec"],["b33f7107.87afa"]]},{"id":"3ee69db9.089ae2","type":"file","z":"19ade742bc711664","name":"debug: image.png","filename":"/data/image.png","appendNewline":true,"createDir":false,"overwriteFile":"true","encoding":"none","x":910,"y":280,"wires":[[]]},{"id":"2a2a43a2.4777ec","type":"debug","z":"19ade742bc711664","name":"ffmeg error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":890,"y":340,"wires":[]},{"id":"2d14dd1f.1b0a22","type":"google-cloud-vision","z":"19ade742bc711664","account":"","keyFilename":"","faceDetection":false,"landmarkDetection":false,"logoDetection":false,"labelDetection":false,"textDetection":true,"documentTextDetection":false,"safeSearchDetection":false,"imageProperties":false,"cropHints":false,"webDetection":false,"productSearch":false,"objectLocalization":false,"name":"OCR","x":850,"y":80,"wires":[["7b7f9813.6b6b28","ab643ce2.503ee"]]},{"id":"7b7f9813.6b6b28","type":"debug","z":"19ade742bc711664","name":"debug ocr","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1120,"y":80,"wires":[]},{"id":"ab643ce2.503ee","type":"function","z":"19ade742bc711664","name":"","func":"if ('textAnnotations' in msg.payload) {\n    if (msg.payload.textAnnotations.length > 0) {\n        return { payload: msg.payload.textAnnotations[0].description };\n    }\n}\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":900,"y":160,"wires":[["64a2e306.daa40c","db6b2394.a398a"]]},{"id":"2815d499.e4ad9c","type":"inject","z":"19ade742bc711664","name":"start","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":130,"y":60,"wires":[["be8d9370.de5a4"]]},{"id":"be8d9370.de5a4","type":"change","z":"19ade742bc711664","name":"config","rules":[{"t":"set","p":"debug","pt":"global","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":310,"y":60,"wires":[[]]},{"id":"64a2e306.daa40c","type":"debug","z":"19ade742bc711664","name":"OCR output","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1130,"y":160,"wires":[]},{"id":"3e42f21b.42a31e","type":"mqtt in","z":"19ade742bc711664","name":"display/button","topic":"display/button","qos":"2","datatype":"auto","broker":"d32bbf31.d1fa5","nl":false,"rap":true,"rh":0,"x":130,"y":300,"wires":[["6adb8e8f.7d5ab","246a2d29.de4ad2"]]},{"id":"26cd96f7.46aeda","type":"switch","z":"19ade742bc711664","name":"Button A?","property":"payload.button","propertyType":"msg","rules":[{"t":"eq","v":"A","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":480,"y":300,"wires":[["a551c355c64013a1"]]},{"id":"db6b2394.a398a","type":"mqtt out","z":"19ade742bc711664","name":"display/text","topic":"display/text","qos":"0","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"d32bbf31.d1fa5","x":1130,"y":240,"wires":[]},{"id":"6adb8e8f.7d5ab","type":"debug","z":"19ade742bc711664","name":"MQTT RX msgs","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":160,"y":400,"wires":[]},{"id":"246a2d29.de4ad2","type":"json","z":"19ade742bc711664","name":"","property":"payload","action":"","pretty":false,"x":330,"y":300,"wires":[["26cd96f7.46aeda"]]},{"id":"b33f7107.87afa","type":"debug","z":"19ade742bc711664","name":"ffmeg output code","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"msg","x":910,"y":400,"wires":[]},{"id":"d32bbf31.d1fa5","type":"mqtt-broker","name":"local MQTT broker","broker":"localhost","port":"1883","clientid":"","usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"sessionExpiry":""}]

Ejecución del comando ffmpeg

Obtener el frame igual que lo hemos hecho en el apartado anterior pero ahora desde dentro de NodeRED es tan sencillo como ejecutar este comando:
Bash
sudo docker run --rm jrottenberg/ffmpeg -y -rtsp_transport tcp -i 'rtsp://admin:ges2ges.@10.2.8.95/h264/ch1/sub/av_stream' -filter:v "crop=704:536:0:20" -vframes 1 -f image2pipe -vcodec png -
Fíjate en el detalle que delante de docker hemos puesto el “sudo” para que se ejecute como root. Por este motivo en el nuevo contendor hemos desplegado un nuevo fichero sudoers, así evitamos tener que introducir el password de root. Cosa compleja en medio de un flujo de NodeRED.
La salida de este nodo será el stream binario que mandaremos a la API de Vision.

Configuración de la API de GCP Vision

Puedes probar la API de Vision de Google desde aquí:
Nosotros la probamos así:
gcp.png
Finalmente gracias a NodeRED usarla fue tan fácil como usar el node-red-contrib-google-cloud:
Concretamente el nodo de Vision.
Como siempre al usar las APIs de Google lo complejo acaba siendo configurar los temas relativos a la seguridad.

Firmware del M5Stack

En este repositorio de github encontrarás el código que hemos usado para programar el M5Stack con Arduino IDE 2.0.0; otras versiones de la IDE deberían funcionar sin problemas.