ROS y Neural Grid Beggar Robot

Por lo general, surgen dos preguntas para tales manualidades: "¿cómo?" ¿y para qué?" La publicación en sí está dedicada a la primera pregunta, y responderé inmediatamente a la segunda:

comencé este proyecto para dominar la robótica, comenzando con la Raspberry Pi y la cámara. Como saben, una de las mejores maneras de aprender algo es idear una tarea técnica e intentar cumplirla, mientras obtienen las habilidades necesarias.

En ese momento, todavía no tenía ideas brillantes en el campo de la robótica, así que decidí hacer un proyecto exclusivamente divertido: un robot mendigo. El resultado es un robot independiente en Raspberry Pi y ROS, que usa el Stick Neural Cumpute de Movidius para detectar rostros. Se pasea por la habitación buscando gente y sacude una lata frente a ellos. Así es como se ve este robot:



El robot se mueve aleatoriamente por la habitación, y si se da cuenta de una persona, se enrolla y sacude un frasco para buscar cosas pequeñas. Por diversión, le agregué una pequeña expresión facial: sabe cómo mover las cejas:



después del primer intento, el robot intenta encontrar su rostro nuevamente a la vista, se vuelve hacia la persona y vuelve a sacudir el banco. Pero qué pasa si te vas en este momento:



Robot


Tomé la idea de un robot de mendicidad de la revista Popular Mechanics . La autoría prototipo de Chris Eckert llamada Gimme se ve muy estéticamente agradable.

imagen

Quería concentrarme más en la funcionalidad, por lo que la caja se ensambló con materiales improvisados. En particular, las esquinas de PVC demostraron ser el material más versátil con el que puede conectar casi dos partes. Parece que en este momento el robot está compuesto en un cinco por ciento por esquinas de PVC y tornillos M3. La caja en sí consiste en tres plataformas laminadas en las que se montan el cabezal y todos los componentes electrónicos.

La base del robot es Raspberry Pi 2B , y el código está escrito en C ++ y se encuentra en GitHub .

Visión


Para percibir la realidad, el robot utiliza la cámara Paspberry Pi Camera Module v2 , que se puede controlar con la biblioteca RaspiCam .

Para la detección de rostros, probé varios enfoques diferentes. La calidad de los detectores clásicos de OpenCV no me satisfizo, así que al final llegué a una solución bastante no estándar. Detección de personas involucradas en la red neuronal, que se ejecutan en el dispositivo Movidius Neural Compute Stick (NCS) bajo el marco de control OpenVINO .

NCS es una pieza de hardware para el lanzamiento efectivo de redes neuronales, dentro de las cuales hay varios procesadores vectoriales especialmente diseñados para esto. El dispositivo está conectado a través de USB y consume solo 1 vatio de energía. Por lo tanto, el NCS actúa como un coprocesador para la Raspberry Pi, que no tira de la red neuronal. Mientras el NCS procesa el siguiente cuadro, el procesador Paspberry es gratuito para otras operaciones. Vale la pena señalar que para un funcionamiento óptimo del dispositivo, se requiere una interfaz USB 3.0, que no está disponible en versiones anteriores de Raspberry; con USB 2.0 también funciona, solo que más lento. Además, para no bloquear los conectores USB Raspberry, conecto el NCS a través de un cable USB corto. Escribí en detalle sobre trabajar con el Neural Compute Stick en mi artículo anterior .

Al principio traté de entrenardetector de rostros propio con arquitectura MobileNet + SSD en conjuntos de datos abiertos. El detector realmente funcionó, pero no muy estable: con el inevitable deterioro de las condiciones de disparo (exposición y disparos borrosos), la calidad del detector se redujo enormemente. Sin embargo, después de un tiempo, aparecieron detectores faciales listos para usar en OpenVINO, y cambié a un detector con la arquitectura SqueezeNet light + SSD , que no solo funcionó mejor en una variedad de condiciones de disparo, sino que también fue más rápido.

Antes de cargar la imagen al NCS para obtener las predicciones del detector, la imagen debe ser preprocesada. El detector de mi elección funciona con imágenes en color.300×300, por lo que la imagen debe comprimirse primero. Para hacer esto, uso el algoritmo de escala más ligero: el método vecino más cercano (INTER_NEAREST en la biblioteca OpenCV). Funciona un poco más rápido que los métodos de interpolación, y casi no afecta el resultado. También vale la pena prestar atención al orden de los canales de imagen: el detector espera el orden de BGR, por lo que debe configurar lo mismo para la cámara.

También intenté separar el procesamiento de video en dos transmisiones, una de las cuales recibió el siguiente cuadro de la cámara y la procesó, y el otro en ese momento cargó el cuadro anterior a NCS y esperó los resultados del detector. Con este esquema, técnicamente, la velocidad de procesamiento aumenta, pero también aumenta el retraso entre la recepción de la trama y la recepción de detecciones. Debido a este retraso en la realidad, el monitoreo de la cara se vuelve más difícil, por lo que al final rechacé este esquema.

Además de detectar rostros, también deben rastrearse para evitar errores del detector. Para hacer esto, utilizo el rastreador liviano Simple Online Realtime Tracker (SORT) . Este rastreador simple consta de dos partes: el algoritmo húngaro se usa para unir objetos en cuadros adyacentes, y para predecir la trayectoria del objeto, si de repente desaparece: filtro de Kalman . Mientras jugaba con el seguimiento facial, descubrí que las trayectorias predichas por el filtro de Kalman pueden ser muy inverosímiles con movimientos repentinos, lo que nuevamente complica el proceso.

Por lo tanto, apagué el filtro de Kalman, dejando solo el algoritmo de coincidencia de caras y el contador del número secuencial de fotogramas en los que se detectó la cara, de esta manera me deshago de los falsos positivos del detector.

Plataforma superior, de izquierda a derecha: cámara, servos para controlar la cabeza y las cejas, interruptor, terminales de alimentación, botón rojo grande.


Tráfico


Para el movimiento, el robot tiene cinco servos: dos servos de rotación continua FS5103R hacen girar las ruedas; Hay dos FS5109M más comunes, uno de los cuales gira la cabeza y el otro sacude la lata; Finalmente, el pequeño SG90 mueve las cejas.

Para ser honesto, los mini servos SG90 me parecieron basura: uno de mis servos tenía el ancho de pulso de control incorrecto y solo uno sobrevivió entre los otros cuatro. Para ser justos, accidentalmente tomé a uno de los sirvientes con el codo, pero los otros dos simplemente no podían soportar la carga (solía usarlos para la cabeza y los bancos). Incluso el último servo, que obtuvo el trabajo más simple: mover las cejas, tiene que pinchar un palo de vez en cuando para que no se acuñe. Con otros servos, no noté ningún problema. Es cierto que los servos de rotación continua a veces tienen que calibrarse para que no giren en el estado inactivo; para esto hay un pequeño regulador que se puede girar con un destornillador de cabeza de reloj.

Resulta que administrar servos con Raspberry no es tan simple. Primero, son controlados pormodulación de ancho de pulso (PWM / PWM) , y en Raspberry solo hay dos pines en los que PWM es compatible con el hardware . En segundo lugar, por supuesto, Raspberry no podrá alimentar los servos, no lo soportará. Afortunadamente, estos problemas se resuelven utilizando un controlador PWM externo.

Adafruit PCA9685 es un controlador PWM de 16 canales que se puede controlar a través de la interfaz I2C . También es muy conveniente que tenga terminales para suministrar energía a los servos. Además, [teóricamente] es posible encadenar hasta 62 controladores, mientras recibe hasta 992 pines de control; para esto, debe asignar una dirección única a cada controlador utilizando puentes especiales. Entonces, si de repente necesitas un ejército de servos, sabes qué hacer.

Para controlar el PCA9685, hay una biblioteca de alto nivel que actúa como una extensión WiringPi. Trabajar con esto es bastante conveniente: durante la inicialización, crea 16 pines virtuales en los que puede escribir una señal PWM, pero primero debe calcular el número de ticks. Para girar la palanca del servo a un cierto ángulo en el rango [0, 180], primero debe traducir este ángulo al rango de longitudes de pulso de control en milisegundos [SERVO_MS_MIN, SERVO_MS_MAX]. Para todos mis servos, estos valores son aproximadamente 0.6 ms y 2.4 ms, respectivamente. En general, estos valores se pueden encontrar en la hoja de datos del servo, pero la práctica ha demostrado que pueden diferir, por lo que puede ser necesario seleccionarlos. Luego divida el valor resultante entre 20 ms (el valor estándar de la duración del ciclo de control) y multiplique por el número máximo de tics PCA9685 (4096):

void driveDegs(float angle, int pin) {
    int ticks = (int) (PCA_MAX_PWM * (angle/180.0f*(SERVO_MS_MAX-SERVO_MS_MIN) + SERVO_MS_MIN) / 20.0f); 
    pwmWrite(pin, ticks);
}

Del mismo modo, esto se hace con servos de rotación continua: en lugar de un ángulo, establecemos la velocidad en el rango [-1,1].

Ensamblé el chasis del robot, así como el cuerpo, por medios improvisados: puse ruedas de muebles en los servomotores de rotación continua, y un soporte de bolas de muebles actúa como la tercera rueda. Anteriormente, en lugar de eso, una rueda se encontraba sobre un soporte giratorio, pero con tal chasis era difícil hacer giros precisos, así que tuve que reemplazarlo. También hay una pequeña rueda debajo de la lata para transferir parte del peso del servo a la carcasa. Al principio, algo simple que no era obvio para mí era que las palancas servo se deben fijar con un tornillo, especialmente para las ruedas, para que no se caigan en el camino. Debido a tal estupidez, tuve que rehacer el chasis una vez. También hice del robot un parachoques ancho hecho de esquinas de PVC para que no se atasque con tanta frecuencia.

Ahora sobre lo que puedes hacer al respecto. En primer lugar, puede sacudir el frasco y mover las cejas; para esto solo tiene que girar la palanca del servo a ángulos preseleccionados.

En segundo lugar, puedes girar la cabeza. No quería que la cabeza girara a la velocidad máxima del servo, porque tiene una cámara. Por lo tanto, decidí reducir la velocidad mediante programación: necesito girar la palanca un ángulo pequeño, luego esperar unos milisegundos, y así sucesivamente hasta alcanzar el ángulo deseado. En este caso, es necesario recordar la posición absoluta actual de la cabeza y cada vez verificar si ha excedido los límites permitidos (en mi robot está en el rango [10, 90] grados).

En tercer lugar, puede cambiar la dirección del movimiento cambiando la velocidad de rotación de las ruedas. Del mismo modo, puede girar la plataforma, por ejemplo, para seguir la cara. La velocidad angular de rotación depende tanto de los servos como de su ubicación en el chasis, por lo que es más fácil medirlo una vez y luego tenerlo en cuenta en las curvas. Para encontrar el retraso necesario entre encender los motores para la rotación y apagarlos, debe dividir el módulo de ángulo por la velocidad angular.

Finalmente, puede girar la cabeza y el chasis simultáneamente y asincrónicamente para no perder el tiempo. Lo hago así:

auto waitRotation = std::async(std::launch::async, rotatePlatform, platformAngle);
success = driveHead(headAngle);
waitRotation.wait();

Plataforma central, de izquierda a derecha: PCA9685, bus de alimentación, Raspberry Pi, MCP3008 ADC


Navegación


Entonces no complicaba nada, por lo que el robot usa solo dos buscadores de alcance infrarrojos Sharp GP2Y0A02YK para la navegación. Esto tampoco es tan simple, porque los sensores son analógicos, pero Raspberry, a diferencia de Arduino, no tiene entradas analógicas. Este problema se resuelve con el convertidor analógico a digital (ADC / ADC): utilizo el MCP3008 de 10 bits y 8 canales. Se vende como un microcircuito separado, por lo que tuvo que soldarse a una placa de circuito impreso y los pines también se soldaron allí para que sea más conveniente conectarse. Además, siguiendo el consejo de mi bati, que se mete más en los circuitos, solde dos condensadores (cerámica y electrolíticos) entre las patas de la fuente de alimentación y el suelo para absorber el ruido de la parte digital de todo el circuito. Los sensores no emiten más de tres voltios en la salida, por lo que se pueden conectar 3.3v con Raspberry como voltaje ADC de referencia (VREF), lo mismo que para la fuente de alimentación MCP3008 (VDD).

El MCP3008 se puede controlar a través de la interfaz SPI , y para esto es incluso más fácil encontrar código listo para usar en GitHub .

A pesar de esto, para un trabajo conveniente con el ADC, necesitará algunos bailes con una pandereta.
unsigned int analogRead(mcp3008Spi &adc, unsigned char channel)
{
    unsigned char spi_data[3];
    unsigned int val = 0;

    spi_data[0] = 1;  // start bit
    spi_data[1] = 0b10000000 | ( channel << 4); // mode and channel
    spi_data[2] = 0; // anything
    adc.spiWriteRead(spi_data, sizeof(spi_data));
  
    // read value, combine last two bits of second byte with whole third byte
    val = (spi_data[1]<< 8) & 0b1100000000; 
    val |= (spi_data[2] & 0xff);
    return val;
}


Se deben enviar tres bytes al MCP3008, donde el bit de inicio se escribe en el primer byte, y el modo y el número de canal (0-7) en el segundo. También recuperamos tres bytes, después de lo cual debemos pegar los dos bits menos significativos del segundo byte con todos los bits del tercero.

Ahora que podemos obtener los valores de los sensores, necesitamos calibrarlos, porque los dos sensores pueden diferir ligeramente entre sí. En general, la visualización desde una distancia debido a la intensidad de la señal de estos sensores no es lineal y no es muy simple ( para más detalles, consulte la hoja de datos, pdf ). Por lo tanto, es suficiente recoger dos coeficientes, cuando se multiplican por los cuales los sensores darán un valor de 1.0 a alguna distancia significativa e igual.

Las lecturas del sensor pueden ser bastante ruidosas, especialmente en obstáculos difíciles, por lo que utilizo un promedio móvil ponderado exponencialmente (EWMA) para suavizar la señal de cada sensor. Seleccioné los parámetros de suavizado a simple vista, para que la señal no haga ruido y no quede muy por detrás de la realidad.

Vista frontal: banco, telémetros y parachoques.


Nutrición


Primero, evalúe qué corriente consumirá el robot ( sobre el consumo actual de Raspberry y periféricos ):

  • Raspberry Pi 2B: no menos de 350 mA, pero más bajo carga (hasta 750-820 mA (?));
  • Cámara: aproximadamente 250 mA;
  • Neural Compute Stick: consumo de energía declarado de 1 vatio, a un voltaje de 5 voltios en USB es de 200 mA;
  • Sensores IR: 33 mA cada uno ( hoja de datos, pdf );
  • MCP3008: , 0.5 (, pdf);
  • PCA9685: , 6 (, pdf);
  • : ~150-200 1500-2000 (stall current), ( FS5109M, pdf)
  • HDMI ( ): 50 ;
  • + ( ): ~200 .

En total, se puede estimar que 1.5-2.5 amperios deberían ser suficientes, siempre que todos los servos no se muevan simultáneamente bajo una carga pesada. Al mismo tiempo, Raspberry necesita un voltaje condicional de 5 voltios y para servos: 4.8-6 voltios. Queda por encontrar una fuente de energía que cumpla con estos requisitos.

Como resultado, decidí alimentar el robot con baterías 18650. Si toma dos baterías ROBITON 3.4 / Li18650 (3.6 voltios, 3400 mAh, corriente de descarga máxima 4875 mA) y las conecta en serie, pueden producir hasta 4.8 amperios a un voltaje de 7.2 voltios. Con una corriente de consumo de 1.5-2.5 amperios, deberían ser suficientes durante una o dos horas.

Las baterías, por cierto, tienen un inconveniente: a pesar del factor de forma indicado 18650, sus tamaños están lejos de ser18×650mm: son varios milímetros más largos debido al circuito de control de carga incorporado. Debido a esto, tuve que apuñalar el compartimento de la batería con un cuchillo para que quepan allí.

Solo queda bajar el voltaje a 5 voltios. Para esto, uso dos convertidores DC-DC reductores separados DFRobot Power Module. Esta pieza de hierro le permite bajar el voltaje a un voltaje de entrada de 3.6-25 voltios y una diferencia de voltaje de al menos 0.6 voltios. Por conveniencia, tiene un interruptor que le permite seleccionar exactamente 5 voltios en la salida, o puede configurar un voltaje de salida arbitrario utilizando un regulador especial. Puse ambos convertidores a 5 voltios; uno de ellos alimenta Raspberry a través de un conector Micro-USB, y el segundo alimenta servos a través de terminales PCA9685. Esto es necesario para maximizar la fuente de alimentación de las partes lógicas y de potencia del robot para que no interfieran entre sí.

En la etapa de depuración, utilicé una fuente de alimentación china de 9 voltios y 2 amperios en lugar de las baterías, y fue suficiente para que el robot funcionara: la conecté, como las baterías, a dos convertidores CC-CC. Por lo tanto, para mayor comodidad, hice terminales en el robot, a los que puede conectar una fuente de alimentación o un compartimento de batería para elegir. Esto ayudó mucho cuando reescribí completamente todo el código en ROS, y tuve que depurar el robot durante mucho tiempo, incluidos los servos.

Por conveniencia, también tuve que hacer un "bus de energía", de hecho, solo un pedazo de la placa con tres filas de pines conectados para tierra, 3.3v y 5v, respectivamente. El bus se conecta a los pines Raspberry correspondientes. Solo los telémetros IR se alimentan desde el bus de 5v, y MCP3008 y PCA9685 desde el bus de 3.3v.

Y, por supuesto, de acuerdo con la vieja tradición, puse el Big Red Button en el robot: cuando se presiona, simplemente interrumpe todo el circuito de alimentación. No fue necesario usarlo para una parada de emergencia, pero encender el robot con la ayuda de un botón es realmente más conveniente.

Plataforma inferior, de izquierda a derecha: compartimento de la batería, convertidores NCS, DC-DC, servoaccionamientos con ruedas, telémetros.


Control de robot


No hay Wi-Fi en la Raspberry Pi 2B, por lo que tengo que conectarme a través de ssh a través de un cable Ethernet (por cierto, esto se puede hacer directamente desde la computadora portátil, sin usar un enrutador ). Resulta este esquema: nos conectamos a través de ssh a través del cable, arrancamos el robot y tiramos del cable. Luego puede regresar a su lugar para acceder a la Frambuesa nuevamente. Hay soluciones más elegantes, pero decidí no complicarme.

Para que el robot pueda detenerse fácilmente sin apagarse, agregué un interruptor soviético masivo (¿desde un submarino?) A él, cuando lo apaga, el programa termina y el robot se detiene.

El conmutador se conecta al suelo y a uno de los pines Raspberry GPIO, y puede leerlo usando la biblioteca WiringPi :

wiringPiSetup();
pinMode(PIN_SWITCH, INPUT);
pullUpDnControl(PIN_SWITCH, PUD_UP);
bool value = digitalRead(BB_PIN_SWITCH);

Vale la pena señalar que con esta conexión, el voltaje en el pin debe elevarse hasta 3.3v, y al mismo tiempo producirá una señal alta en el estado abierto y una señal baja en el estado cerrado.

Poniendolo todo junto


Subprocesos

Ahora, todo lo anterior debe combinarse en un solo programa que controle el robot. En la primera versión del robot, hice esto usando hilos ( pthread ). Esta versión está en la rama maestra , pero el código allí es bastante aterrador.

El programa funciona en cuatro subprocesos: un subproceso toma fotogramas de la cámara e inicia el detector en el NCS; el segundo flujo lee datos de telémetros; el tercer subproceso supervisa el conmutador y establece la variable global is_runningenfalsesi está apagado El hilo principal es responsable del comportamiento del robot y del servo control. Los hilos tienen punteros en común con el hilo principal, por el cual escriben los resultados de su trabajo. Limité los vectores que almacenan información sobre las caras encontradas por el detector al mutex, y declaró las otras variables comunes más simples como atómicas. Para coordinar el flujo del detector de rostros con el hilo principal, hay un indicador face_processedque se restablece cuando sale un nuevo resultado del detector, y se eleva cuando el hilo principal usa este resultado para seleccionar un comportamiento; esto es necesario para no procesar datos antiguos que pueden no ser relevantes despues de mudarse.

La

versión ROS con streams funcionó bien, pero comencé todo esto para aprender algo, entonces ¿por qué no al mismo tiempo master?Ros ? He estado escuchando este marco durante mucho tiempo, e incluso tuve que trabajar un poco con él en un hackathon, así que al final decidí volver a escribir todo el código en ROS. Esta versión del código se encuentra en la rama predeterminada de ros y se ve mucho más decente. Está claro que la implementación en ROS seguramente será más lenta que la implementación en los flujos debido a la sobrecarga de enviar mensajes y todo lo demás; la única pregunta es ¿cuánto?

Concepto ROS
ROS (Robot Operating System) — , , , .

, , , (node), , , .

(topic) (message) , - .

— (service). , , . « », .

.msg .srv . .

ROS .

Para mi robot, no utilicé ningún paquete listo con algoritmos de ROS, solo diseñé el código del robot en un paquete separado que consta de cinco nodos que se comunican entre sí mediante mensajes y servicios de ROS.

El nodo más simple switch_node, supervisa el estado del conmutador. Tan pronto como se apaga el interruptor, el nodo comienza a enviar mensajes no informativos del tipo boolen el tema terminator. Esta es una señal para el nodo principal de que es hora de completar el trabajo.

El segundo nodo, sensor_nodelee periódicamente las lecturas de ambos telémetros IR y los envía al tema en sensor_stateun mensaje. Además, este nodo es responsable del procesamiento de la señal: escalado por factores de calibración y promedio móvil.

Tercer nudocamera_nodeÉl es responsable de todo lo relacionado con las caras: toma imágenes de la cámara, las procesa, recibe los resultados del detector, las pasa a través del rastreador y luego encuentra la cara más cercana al centro del marco: el robot no usa el resto de todos modos, pero desea hacer mensajes más pequeños. Los mensajes que el nodo envía al tema camera_statecontienen el número de cuadro, el hecho de tener una cara (porque también necesita saber sobre la ausencia de una cara), las coordenadas relativas de la esquina superior izquierda, el ancho y la altura de la cara. Así es como se ve la descripción del tipo de mensaje en el archivo DetectionBox.msg:

int64 count
bool present
float32 x
float32 y
float32 width
float32 height

El cuarto nodo, servo_nodees responsable de los servos. En primer lugar, admite un servicio servo_actionque permite que una de las acciones sea realizada por los servos por su número: poner todo el nodo en su estado inicial (cejas, banco, cabeza, detener el chasis); transfiere la cabeza a su estado inicial; agite el frasco; representa con una ceja una de las tres expresiones (buena, neutral, mala). En segundo lugar, utilizando el servicio, servo_speedpuede establecer nuevas velocidades para ambas ruedas enviándolas en la solicitud. Ambos servicios no devuelven nada. Finalmente, hay un servicio servo_head_platformque le permite rotar el cabezal y / o el chasis un cierto ángulo con respecto a la posición actual. Este servicio regresa truesi fue posible girar la cabeza al menos parcialmente, yfalsede lo contrario, en el caso en que la cabeza ya esté en el borde del ángulo permitido, y estamos tratando de girarla aún más. Si ambos ángulos en la solicitud son distintos de cero, el servicio gira de forma asincrónica, como se indicó anteriormente. En el bucle principal, el servo nodo no hace nada.

Aquí, por ejemplo, hay una descripción del servicio servo_head_platform:

float32 head_delta
float32 platform_delta
---
bool head_success

Cada uno de los nodos enumerados admite un servicio terminate_{switch, camera, sensor, servo}con una solicitud de respuesta vacía, que detiene la operación del nodo. Se implementa de esta manera:

Algun codigo
...
std::atomic_bool is_running; // global

bool terminate_node(std_srvs::Empty::Request &req, std_srvs::Empty::Response &ignored) {
    is_running = false;
    return true;
}

int main(int argc, char **argv) {
    is_running = true;
    ...
    while (is_running && ros::ok()) {
        // do stuff
    }
    ...
}


El nodo tiene una variable global is_running, cuyo valor determina el ciclo principal del nodo. El servicio simplemente restablece esta variable y se interrumpe el bucle principal.

También hay un nodo principal beggar_boten el que se implementa la lógica básica del robot. Antes del inicio del bucle principal, se suscribe a temas sensor_statey camera_stateguarda el contenido de los mensajes en variables globales en las funciones de devolución de llamada. También está suscrito al tema terminator, cuya devolución de llamada restablece el indicador is_runninge interrumpe el bucle principal. Además, antes de que comience el ciclo, el nodo anuncia las interfaces para los servicios del servo nodo y espera unos segundos a que se inicien los otros nodos. Después de que finaliza el bucle principal, este nodo llama a los serviciosterminate_{switch, camera, sensor, servo}, apagando todos los demás nodos y luego apagándolo a sí mismo. Es decir, cuando el interruptor está apagado, los cinco nodos completan la operación.

Cambiar a ROS me obligó a cambiar bastante la estructura del programa. Por ejemplo, antes era posible cambiar la velocidad de la rueda con una frecuencia alta, y esto funcionó bien, pero el servicio ROS funciona un orden de magnitud más lento, por lo que tuve que volver a escribir el código para que se llamara al servicio solo cuando la velocidad realmente cambia (en "modo perezoso").

ROS también le permite ejecutar de manera bastante conveniente todos los nodos del robot. Para hacer esto, debe escribir un archivo de lanzamiento .launch que enumere todos los nodos y otros atributos del robot en formato xml, y luego ejecute el comando:

roslaunch beggar_bot robot.launch

ROS vs. pthread

Ahora, finalmente, puede comparar la velocidad de la versión ROS y la versión pthread. Lo hago de esta manera: el hilo / nodo responsable de trabajar con la cámara considera su FPS (como el elemento más lento), siempre que todo lo demás también funcione. Para la versión pthread, constantemente recibí FPS 9.99 más o menos, para la versión ROS resultó aproximadamente 8.3. De hecho, esto es suficiente para tal juguete, pero la sobrecarga es bastante notable.

Comportamiento del robot


La idea es bastante simple: si el robot ve a una persona, debe conducir hacia él y agitar la lata. Agitar el frasco es bastante simple y divertido, pero primero debes llegar a la persona.

Existe una función follow_faceque, si hay una cara en el marco, gira el chasis y la cabeza del robot en su dirección (solo se tiene en cuenta la cara más cercana al centro). Esto es necesario para que el robot siempre siga su curso en una persona, si está en el marco, y también mire directamente a la cara cuando agite un frasco.

La camera_statemisma variable se utiliza para sincronizar esta función con el tema .face_processed, como en la versión con secuencias. La idea es la misma: queremos procesar datos solo una vez, porque el robot está en constante movimiento. La función primero espera hasta que la devolución de llamada del tema con las detecciones baje el indicador de que se ha procesado el último fotograma. Mientras espera, llama constantemente ros::spinOnce()para recibir nuevos mensajes (en general, esto debe hacerse siempre que el programa espere nuevos datos). Si hay una cara en el marco, se calculan los ángulos, que deben rotar la plataforma y la cabeza, esto se puede hacer conociendo las coordenadas relativas del centro de la cara y el campo de visión de la cámara horizontal y verticalmente. Después de eso, puede llamar al servicio servo_head_platformy mover el robot.

Hay un punto sutil: la información sobre la posición de la cara se retrasa detrás del movimiento real de la cara y puede retrasarse respecto de los movimientos del propio robot. Por lo tanto, el robot puede sobreestimar el ángulo de rotación, por lo que la cabeza comienza a moverse hacia adelante y hacia atrás con una amplitud creciente. Para evitar esto, hago retrasos después del movimiento (300 ms), y también omito un cuadro después del movimiento. Para el mismo propósito, los ángulos de rotación del chasis y la cabeza se multiplican por un factor de 0.8 (los componentes P del controlador PID tienen sentido ).

Funciónfollow_facedevuelve el estado de una persona. Una persona puede: estar ausente, estar lo suficientemente cerca del centro, estar demasiado lejos del robot; otra opción: cuando giramos el robot y no sabemos qué pasó con la cara (en el proceso de búsqueda); Todavía hay un caso raro cuando la cabeza está en el borde, por lo que es imposible voltear hacia la cara.

Algo bastante simple sucede en el bucle principal:

  1. Llame follow_facehasta que la persona tenga un cierto estado (cualquiera, excepto "en el proceso de búsqueda"). Al final de este paso, el robot mirará directamente a la cara.
  2. Si se encuentra la cara y está cerca:
    1. Agite la lata;
    2. Encuentra la cara otra vez;
    3. Si la cara está en su lugar, haga una buena expresión con las cejas y agite el frasco nuevamente;
    4. Si la cara ha desaparecido, haga una expresión enojada con las cejas;
    5. Date la vuelta, ve al comienzo del ciclo.

  3. Si no hay una persona (o está muy lejos), navegue por la habitación:
    1. Si hay lejos de obstáculos en ambos lados, conduzca hacia adelante (si se encontró la cara, pero resultó estar demasiado lejos, el robot irá hacia la persona);
    2. Si los obstáculos están cerca en ambos lados, haga un giro aleatorio en el rango [90,180][180,90];
    3. Si el obstáculo está solo en un lado, gire en la dirección opuesta en un ángulo aleatorio [0,90];
    4. Si el movimiento hacia adelante continúa durante demasiado tiempo (posiblemente atascado), retroceda un poco y gire al azar en el rango [90,180][180,90];


Este algoritmo no pretende ser una inteligencia artificial fuerte, sin embargo, el comportamiento aleatorio y un parachoques ancho permiten que el robot salga de casi cualquier posición, tarde o temprano.

Conclusión


A pesar de su aparente simplicidad, este proyecto cubre muchos temas no triviales: trabajar con sensores analógicos, trabajar con PWM, visión por computadora, coordinación de tareas asincrónicas. Además, es increíblemente divertido. Probablemente, además haré algo más significativo, pero más con un sesgo en el aprendizaje profundo.

Como beneficio adicional, la galería:








All Articles