/
IoT con Vision Google API
Search
Duplicate
Try Notion
IoT con Vision Google API
Fecha publicación
October 28, 2022
Dificultad
Intermedio
#
36
3 more properties
Descripción
Disponemos de este descalcificador instalado en el garaje:
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í:
Para los que no conozcaís M5Stack tiene esta pinta:
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.
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:
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:
http://10.2.8.95:8000
Bash
Dentro del ONVIF manager podremos descubrir la dirección RTSP:
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í:
rtsp://admin:ges2ges.@10.2.8.95/h264/ch1/sub/av_stream
Bash
Aquí podemos ver el RTSP mostrando el stream de vídeo:
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:
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 -
Bash
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:
docker run --rm jrottenberg/ffmpeg → este trozo solo sirve para lanzar el contendor de docker, sería equivalente a ejecutar el comando si tubíeramos instalada la aplicación.
-y → a cualquier pregunta que nos haga el programa le responderemos “yes”. Así evitamos que sea interactiva su ejecución.
-rtsp_transport tcp → informamos que vamos a capturar un stream RTSP que va sobre TCP
-i 'rtsp://admin:ges2ges.@10.2.8.95/h264/ch1/sub/av_stream' → indicamos la URL del stream RTSP que vamos a consumir
-filter:v "crop=704:536:0:20" → recortamos la imagen para que no se vea el texto que aparece en la parte superior e inferior de la imagen indicando la marca de tiempo y nombre de la cámara. Queremos evitar que después la API de VISION decodifique estos carácteres.
-vframes 1 → indicamos que solo queremos capturar un frame
-f image2pipe -vcodec png - → queremos que la salida del frame sea en formato imagen, concretamente en formato PNG y que salga por pantalla (standard output), esto lo marca el último guión. Si cambias este guión por un nombre de fichero tendrás la imagen en el fichero.
La imagen capturada tiene este aspecto:
Creación de imagen Docker de NodeRED
✅
Asumimos que tienes docker y docker-compose instalados en la máquina donde vayas a ejecutar estos comandos.
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:
docker-compose build
Bash
Dockerfile
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
root ALL=(ALL:ALL) ALL
%adm ALL=(ALL:ALL) NOPASSWD:ALL
@includedir /etc/sudoers.d
docker-compose.yml
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
Código del flow
[{"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:
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 -
Bash
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í:
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.