Robô ROS e Mendigo de Grade Neural

Geralmente, duas dessas questões surgem para tais ofícios: "como?" e para quê?" A publicação em si é dedicada à primeira pergunta, e responderei imediatamente à segunda:

iniciei este projeto para dominar a robótica, começando com o Raspberry Pi e a câmera. Como você sabe, uma das melhores maneiras de aprender algo é criar uma tarefa técnica e tentar cumpri-la, enquanto obtém as habilidades necessárias.

Naquela época, eu ainda não tinha idéias brilhantes no campo da robótica, então decidi fazer um projeto exclusivamente divertido - um robô mendigo. O resultado é um robô autônomo no Raspberry Pi e ROS, usando o Movidius Neural Cumpute Stick para detectar rostos. Ele anda pela sala, procurando por pessoas, e balança uma lata na frente deles. Aqui está a aparência desse robô:



O robô se move aleatoriamente pela sala e, se notar uma pessoa, rola até ele e sacode uma jarra para coisas pequenas. Por diversão, adicionei uma pequena expressão facial a ele - ele sabe como mover as sobrancelhas:



após a primeira tentativa, o robô tenta encontrar seu rosto novamente à vista, vira-se para a pessoa e sacode o banco novamente. Mas o que acontece se você sair neste momento:



Robô


Tomei a idéia de um robô implorador da revista Popular Mechanics . A autoria do protótipo de Chris Eckert, chamada Gimme, parece muito esteticamente agradável.

imagem

Como eu queria me concentrar mais na funcionalidade, o gabinete foi montado a partir de materiais improvisados. Em particular, os cantos de PVC provaram ser o material mais versátil com o qual você pode conectar quase duas partes. Parece que no momento o robô é composto por cinco por cento de cantos de PVC e parafusos M3. O gabinete em si consiste em três plataformas laminadas nas quais a cabeça e todos os componentes eletrônicos estão montados.

A base do robô é o Raspberry Pi 2B , e o código está escrito em C ++ e está no GitHub .

Visão


Para perceber a realidade, o robô usa a câmera Paspberry Pi Camera Module v2 , que pode ser controlada usando a biblioteca RaspiCam .

Para detecção de rosto, tentei várias abordagens diferentes. A qualidade dos detectores clássicos da OpenCV não me satisfazia, então, no final, cheguei a uma solução não padronizada. Detecção de pessoas envolvidas na rede neural em execução no dispositivo Movidius Neural Compute Stick (NCS) sob a estrutura de controle OpenVINO .

O NCS é uma peça de hardware para o lançamento eficaz de redes neurais, dentro da qual existem vários processadores vetoriais especialmente adaptados para isso. O dispositivo está conectado via USB e consome apenas 1 Watt de energia. Assim, o NCS atua como um coprocessador para o Raspberry Pi, que não puxa a rede neural. Enquanto o NCS está processando o próximo quadro, o processador Paspberry está livre para outras operações. Vale ressaltar que, para uma operação ideal do dispositivo, é necessária uma interface USB 3.0, que não está disponível nas versões mais antigas do Raspberry; com o USB 2.0, ele também funciona, apenas mais devagar. Além disso, para não bloquear os conectores USB Raspberry, conecto o NCS a ele através de um cabo USB curto. Escrevi em detalhes sobre como trabalhar com o Neural Compute Stick no meu artigo anterior .

No começo eu tentei treinardetector de rosto próprio com arquitetura MobileNet + SSD em conjuntos de dados abertos. O detector realmente funcionou, mas não é muito estável: com a inevitável deterioração das condições de disparo (exposição e fotos desfocadas), a qualidade do detector diminuiu bastante. No entanto, depois de algum tempo, os detectores de rosto prontos apareceram no OpenVINO, e eu mudei para um detector com a arquitetura SqueezeNet light + SSD , que não só funcionou melhor em várias condições de disparo, mas também foi mais rápido.

Antes de carregar a imagem no NCS para obter as previsões do detector, a imagem deve ser pré-processada. O detector de minha escolha funciona com imagens coloridas300×300, para que a imagem precise ser compactada primeiro. Para fazer isso, eu uso o algoritmo de dimensionamento mais leve - o método vizinho mais próximo (INTER_NEAREST na biblioteca OpenCV). Funciona um pouco mais rápido que os métodos de interpolação e quase não afeta o resultado. Também vale a pena prestar atenção na ordem dos canais de imagem: o detector espera a ordem BGR, portanto, é necessário definir o mesmo para a câmera.

Também tentei separar o processamento de vídeo em dois fluxos, um dos quais recebeu o próximo quadro da câmera e o processou, e o outro naquele momento carregou o quadro anterior no NCS e esperou pelos resultados do detector. Com esse esquema, tecnicamente, a velocidade de processamento aumenta, mas o atraso entre o recebimento do quadro e o recebimento de detecções também aumenta. Por causa desse atraso na realidade, monitorar o rosto se torna apenas mais difícil, então, no final, recusei esse esquema.

Além de realmente detectar rostos, eles também precisam ser rastreados para evitar erros no detector. Para fazer isso, eu uso o rastreador leve Simple Online Realtime Tracker (SORT) . Este rastreador simples consiste em duas partes: o algoritmo húngaro é usado para combinar objetos em quadros adjacentes, e para prever a trajetória do objeto, se ele desaparecer repentinamente - filtro Kalman . Enquanto brincava com o rastreamento de rostos, descobri que as trajetórias previstas pelo filtro Kalman podem ser muito implausíveis com movimentos bruscos, o que novamente apenas complica o processo.

Portanto, desliguei o filtro Kalman, deixando apenas o algoritmo de correspondência de faces e o contador do número seqüencial de quadros em que a face foi detectada - desta maneira, me livrei dos falsos positivos do detector.

Plataforma superior, da esquerda para a direita: câmera, servos para controlar a cabeça e as sobrancelhas, interruptor, terminais de energia, botão vermelho grande.


Tráfego


Para movimentação, o robô possui cinco servos: dois servos de rotação contínua FS5103R giram as rodas; Existem mais dois FS5109Ms comuns, um dos quais gira a cabeça e o outro sacode a lata; finalmente, o pequeno SG90 move as sobrancelhas.

Para ser honesto, os mini-servos SG90 pareciam lixo para mim - um dos meus servos tinha a largura de pulso de controle incorreta e apenas um sobreviveu entre os outros quatro. Para ser justo, acidentalmente levei um dos criados com o cotovelo, mas os outros dois simplesmente não aguentavam a carga (eu costumava usá-los na cabeça e na lata). Até o último servo, que conseguiu o trabalho mais simples - mover as sobrancelhas, tem que enfiar um pedaço de pau de vez em quando, para que não cunha. Com outros servos, não notei nenhum problema. É verdade que, às vezes, os servos de rotação contínua precisam ser calibrados para que não girem no estado inativo - para isso, existe um pequeno regulador neles que pode ser girado com uma chave de fenda.

Gerenciar servos com Raspberry, ao que parece, não é tão simples. Primeiro, eles são controlados pormodulação de largura de pulso (PWM / PWM) e no Raspberry existem apenas dois pinos nos quais o PWM é suportado por hardware . Em segundo lugar, é claro, o Raspberry não será capaz de alimentar os servos, não suportará isso. Felizmente, esses problemas são resolvidos usando um controlador PWM externo.

O Adafruit PCA9685 é um controlador PWM de 16 canais que pode ser controlado via interface I2C . Também é muito conveniente que tenha terminais para fornecer energia para servos. Além disso, [teoricamente] é possível encadear até 62 controladores enquanto recebe até 992 pinos de controle - para isso, é necessário atribuir um endereço exclusivo a cada controlador usando jumpers especiais. Então, se você precisa de um exército de servos de repente, sabe o que fazer.

Para controlar o PCA9685, existe uma biblioteca de alto nível que atua como uma extensão WiringPi. Trabalhar com isso é bastante conveniente - durante a inicialização, ele cria 16 pinos virtuais nos quais você pode gravar um sinal PWM, mas primeiro você precisa calcular o número de ticks. Para girar a alavanca do servo para um determinado ângulo na faixa [0, 180], primeiro você deve converter esse ângulo na faixa de comprimentos de pulso de controle em milissegundos [SERVO_MS_MIN, SERVO_MS_MAX]. Para todos os meus servos, esses valores são aproximadamente 0,6 ms e 2,4 ms, respectivamente. Em geral, esses valores podem ser encontrados na folha de dados do servo, mas a prática demonstrou que eles podem diferir e, portanto, podem precisar ser selecionados. Em seguida, divida o valor resultante por 20 ms (o valor padrão da duração do ciclo de controle) e multiplique pelo número máximo de ticks 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);
}

Da mesma forma, isso é feito com servos de rotação contínua - em vez de um ângulo, definimos a velocidade no intervalo [-1,1].

Montei o chassi do robô, bem como o corpo, a partir de meios improvisados: coloquei rodas de móveis nos servocontroladores de rotação contínua e um suporte de bola de móveis atua como a terceira roda. Anteriormente, em vez disso, havia uma roda em um suporte rotativo, mas com esse chassi era difícil fazer curvas precisas, então tive que substituí-lo. Há também uma pequena roda sob a lata para transferir parte do peso do servo para a carcaça. Uma coisa simples que não me era óbvia inicialmente foi que as servo-alavancas devem ser fixadas com um parafuso, especialmente para as rodas, para que não caiam ao longo do caminho. Por causa dessa estupidez, tive que refazer o chassi uma vez. Também fiz do robô um pára-choque amplo feito de cantos de PVC, para que não ficasse preso com tanta frequência.

Agora, sobre o que você pode fazer sobre isso. Primeiramente, você pode agitar a jarra e mover as sobrancelhas - para isso, basta girar a alavanca do servo para ângulos pré-selecionados.

Em segundo lugar, você pode girar sua cabeça. Eu não queria que a cabeça girasse na velocidade máxima do servo, porque ela tem uma câmera. Portanto, decidi reduzir programaticamente a velocidade: preciso girar a alavanca em um pequeno ângulo e esperar alguns milissegundos - e assim por diante até que o ângulo desejado seja atingido. Nesse caso, é necessário lembrar a posição absoluta atual da cabeça e sempre verificar se ela excedeu os limites permitidos (no meu robô, ela está na faixa de [10, 90] graus).

Em terceiro lugar, você pode alterar a direção do movimento alterando a velocidade de rotação das rodas. Da mesma maneira, você pode girar a plataforma, por exemplo, para seguir a face. A velocidade angular de rotação depende tanto dos próprios servos quanto da localização no chassi; portanto, é mais fácil medi-lo uma vez e levá-lo em consideração nas curvas. Para encontrar o atraso necessário entre ligar os motores para rotação e desligá-los, é necessário dividir o módulo de ângulo pela velocidade angular.

Por fim, você pode girar a cabeça e o chassi simultaneamente e de forma assíncrona para não perder tempo. Eu faço assim:

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

Plataforma central, da esquerda para a direita: PCA9685, barramento de força, Raspberry Pi, MCP3008 ADC


Navegação


Como eu não compliquei nada, o robô usa apenas dois telêmetros infravermelhos Sharp GP2Y0A02YK para navegação. Isso também não é tão simples, porque os sensores são analógicos, mas o Raspberry, ao contrário do Arduino, não possui entradas analógicas. Esse problema foi resolvido pelo conversor analógico-digital (ADC / ADC) - eu uso o MCP3008 de 10 bits e 8 canais. Ele é vendido como um microcircuito separado, portanto teve que ser soldado a uma placa de circuito impresso e os pinos também foram soldados lá para tornar a conexão mais conveniente. Além disso, seguindo o conselho do meu bati, que se atrapalha mais em circuitos, soldei dois capacitores (cerâmicos e eletrolíticos) entre as pernas da fonte de alimentação e o solo para absorver o ruído da parte digital de todo o circuito. Os sensores produzem não mais que três volts na saída; portanto, 3.3v com Raspberry pode ser conectado como uma tensão ADC de referência (VREF) - a mesma da fonte de alimentação MCP3008 (VDD).

O MCP3008 pode ser controlado pela interface SPI e, para isso, é fácil encontrar código pronto no GitHub .

Apesar disso, para um trabalho conveniente com o ADC, você precisará de algumas danças com um pandeiro.
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;
}


Três bytes devem ser enviados para o MCP3008, onde o bit inicial é gravado no primeiro byte e o número do modo e do canal (0-7) no segundo. Também recebemos três bytes, após o qual precisamos colar os dois bits menos significativos do segundo byte com todos os bits do terceiro byte.

Agora que podemos obter os valores dos sensores, precisamos calibrá-los, porque os dois sensores podem diferir ligeiramente um do outro. Em geral, a exibição à distância devido à intensidade do sinal desses sensores não é linear e não é muito simples ( para mais detalhes, consulte a folha de dados em pdf ). Portanto, basta captar dois coeficientes, quando multiplicados pelos quais os sensores fornecerão um valor de 1,0 a uma distância igual e significativa.

As leituras dos sensores podem ser bastante barulhentas, especialmente em obstáculos difíceis, então eu uso uma média móvel ponderada exponencialmente (EWMA) para suavizar o sinal de cada sensor. Selecionei os parâmetros de suavização a olho nu, para que o sinal não faça barulho e não fique muito atrás da realidade.

Vista frontal: banco, telémetros e pára-choques.


Nutrição


Primeiro, vamos avaliar qual corrente o robô consumirá ( sobre o consumo atual de framboesa e periféricos ):

  • Raspberry Pi 2B: não inferior a 350 mA, mas mais sob carga (até 750-820 mA (?));
  • Câmera: cerca de 250 mA;
  • Vara de computação neural: consumo de energia declarado de 1 watt, a uma voltagem de 5 volts no USB é de 200 mA;
  • Sensores IR: 33 mA cada ( folha de dados, pdf );
  • MCP3008: , 0.5 (, pdf);
  • PCA9685: , 6 (, pdf);
  • : ~150-200 1500-2000 (stall current), ( FS5109M, pdf)
  • HDMI ( ): 50 ;
  • + ( ): ~200 .

No total, pode-se estimar que 1,5-2,5 amperes devem ser suficientes, desde que todos os servos não se movam simultaneamente sob carga pesada. Ao mesmo tempo, o Raspberry precisa de 5 volts condicionais de voltagem e para servos - 4,8-6 volts. Resta encontrar uma fonte de energia que atenda a esses requisitos.

Como resultado, decidi alimentar o robô com baterias 18650. Se você pegar duas baterias ROBITON 3.4 / Li18650 (3,6 volts, 3400 mAh, corrente máxima de descarga 4875 mA) e conectá-las em série, elas podem produzir até 4,8 amperes a uma voltagem de 7,2 volts. Com uma corrente de consumo de 1,5-2,5 amperes, eles devem ser suficientes por uma hora ou duas.

As baterias, por sinal, têm um problema: apesar do fator de forma indicado 18650, seus tamanhos estão longe de18×650mm - são vários milímetros mais longos devido ao circuito de controle de carregamento embutido. Por causa disso, tive que esfaquear o compartimento da bateria com uma faca, para que eles se encaixassem lá.

Resta apenas baixar a tensão para 5 volts. Para isso, eu uso dois conversores DC-DC reduzidos separados DFRobot Power Module. Este pedaço de ferro permite diminuir a tensão com uma tensão de entrada de 3,6-25 volts e uma diferença de tensão de pelo menos 0,6 volts. Por conveniência, ele possui uma chave que permite selecionar exatamente 5 volts na saída ou configurar uma tensão de saída arbitrária usando um regulador especial. Defino os dois conversores em 5 volts; um deles alimenta o Raspberry através de um conector Micro-USB e o segundo alimenta servos através dos terminais PCA9685. Isso é necessário para maximizar a fonte de alimentação das partes lógicas e de energia do robô, para que elas não interfiram.

No estágio de depuração, usei uma fonte de alimentação chinesa de 9 volts e 2 ampères em vez das baterias, e foi suficiente para o robô funcionar - eu a conectei, como as baterias, a dois conversores DC-DC. Portanto, por conveniência, criei terminais no robô, nos quais você pode conectar uma fonte de alimentação ou compartimento de bateria para escolher. Isso ajudou muito quando eu reescrevi completamente todo o código no ROS, e tive que depurar o robô por um longo tempo, incluindo servos.

Por conveniência, eu também tive que fazer um "barramento de força" - na verdade, apenas um pedaço da placa com três fileiras de pinos conectados para terra, 3,3v e 5v, respectivamente. O barramento se conecta aos pinos correspondentes de framboesa. Somente rangefinders IR são alimentados a partir do barramento 5v, e MCP3008 e PCA9685 a partir do barramento 3.3v.

E, é claro, de acordo com a boa e velha tradição, eu coloco o Grande Botão Vermelho no robô - quando pressionado, ele simplesmente interrompe todo o circuito de potência. Não era necessário usá-lo para uma parada de emergência, mas ligar o robô com a ajuda de um botão é realmente mais conveniente.

Plataforma inferior, da esquerda para a direita: compartimento da bateria, conversores NCS, DC-DC, servomotores com rodas, telémetros.


Controle de robô


Não há Wi-Fi no Raspberry Pi 2B, então eu tenho que conectar via ssh através de um cabo Ethernet (a propósito, isso também pode ser feito diretamente do laptop, sem o uso de um roteador ). Acontece que este esquema: nós nos conectamos via ssh através do cabo, ligamos o robô e o retiramos. Em seguida, ele pode ser retornado ao seu lugar para acessar o Raspberry novamente. Existem soluções mais elegantes, mas decidi não complicar.

Para que o robô possa ser facilmente parado sem desligar, eu adicionei um grande interruptor soviético (de um submarino?). Quando você o desliga, o programa termina e o robô para.

O switch se conecta ao terra e a um dos pinos do Raspberry GPIO, e você pode ler usando a biblioteca WiringPi :

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

É importante notar que, com essa conexão, a tensão no pino deve ser puxada para 3,3v e, ao mesmo tempo, produzirá um sinal alto no estado aberto e um sinal baixo no estado fechado.

Juntando tudo


Tópicos

Agora, todas as opções acima precisam ser combinadas em um programa que controla o robô. Na primeira versão do robô, eu fiz isso usando threads ( pthread ). Esta versão está no ramo principal , mas o código é bastante assustador.

O programa funciona em quatro threads: um thread tira quadros da câmera e inicia o detector no NCS; o segundo fluxo lê dados de rangefinders; o terceiro segmento monitora o comutador e define a variável global is_runningcomofalsese estiver desligado; O thread principal é responsável pelo comportamento do robô e pelo servo controle. Os threads têm ponteiros em comum com o thread principal, pelo qual eles escrevem os resultados de seu trabalho. Limitei os vetores que armazenam informações sobre as faces encontradas pelo detector no mutex e declarei as outras variáveis ​​comuns mais simples como atômicas. Para coordenar o fluxo do detector de rosto com a rosca principal, há um sinalizador face_processedque é redefinido quando um novo resultado vem do detector e sobe quando a rosca principal usa esse resultado para selecionar um comportamento - isso é necessário para não processar dados antigos que podem não ser relevantes depois de se mudar.

A

versão ROS com fluxos funcionou bem, mas eu comecei tudo isso para aprender algo, então por que não ao mesmo tempo mestreRos ? Eu escuto esse framework há muito tempo, e até tive que trabalhar um pouco com ele em um hackathon, então no final decidi reescrever todo o código no ROS. Esta versão do código está no ramo padrão do ros e parece muito mais decente. É claro que a implementação no ROS quase certamente será mais lenta que a implementação nos fluxos devido à sobrecarga de envio de mensagens e tudo mais - a única questão é quanto?

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

, , , (node), , , .

(topic) (message) , - .

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

.msg .srv . .

ROS .

Para o meu robô, não usei nenhum pacote pronto com algoritmos do ROS, apenas projetei o código do robô em um pacote separado que consiste em cinco nós se comunicando usando mensagens e serviços ROS.

O nó mais simples switch_node, monitora o estado do comutador. Assim que o comutador é desligado, o nó começa a enviar mensagens não informativas do tipo boolno tópico terminator. Este é um sinal para o nó principal de que está na hora de concluir o trabalho.

O segundo nó ,, sensor_nodelê periodicamente as leituras dos dois rangefinders de infravermelho e as envia para o tópico em sensor_stateuma mensagem. Além disso, este nó é responsável pelo processamento do sinal: escala por fatores de calibração e média móvel.

Terceiro nócamera_nodeEle é responsável por tudo relacionado aos rostos: ele tira imagens da câmera, processa-os, recebe os resultados do detector, passa-os pelo rastreador e encontra o rosto mais próximo do centro do quadro - o robô não usa o resto de qualquer maneira, mas você deseja fazer mensagens menores. As mensagens que o nó envia para o tópico camera_statecontêm o número do quadro, o fato de ter uma face (porque você também precisa saber sobre a ausência de uma face), as coordenadas relativas do canto superior esquerdo, a largura e a altura da face. É assim que a descrição do tipo de mensagem no arquivo se parece DetectionBox.msg:

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

O quarto nó servo_nodeé responsável pelos servos. Em primeiro lugar, suporta um serviço servo_actionque permite que uma das ações seja executada pelos servos por seu número: colocar todo o nó em seu estado inicial (sobrancelhas, banco, cabeça, parar o chassi); transferir a cabeça para seu estado inicial; agite o frasco; retrate com uma sobrancelha uma das três expressões (boa, neutra, má). Em segundo lugar, usando o serviço, servo_speedvocê pode definir novas velocidades para as duas rodas enviando-as na solicitação. Ambos os serviços não retornam nada. Finalmente, existe um serviço servo_head_platformque permite girar a cabeça e / ou o chassi em um determinado ângulo em relação à posição atual. Este serviço retorna truese for possível girar a cabeça pelo menos parcialmente efalsecaso contrário, no caso em que a cabeça já esteja na borda do ângulo admissível, e estamos tentando movê-la ainda mais. Se os dois ângulos da solicitação forem diferentes de zero, o serviço girará assincronamente, conforme indicado acima. No loop principal, o nó servo não faz nada.

Aqui, por exemplo, está uma descrição do serviço servo_head_platform:

float32 head_delta
float32 platform_delta
---
bool head_success

Cada um dos nós listados suporta um serviço terminate_{switch, camera, sensor, servo}com uma solicitação de resposta vazia, que interrompe a operação do nó. É implementado desta maneira:

Algum código
...
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
    }
    ...
}


O nó possui uma variável global is_running, cujo valor determina o ciclo principal do nó. O serviço simplesmente redefine essa variável e o loop principal é interrompido.

Há também um nó principal beggar_botno qual a lógica básica do robô é implementada. Antes do início do loop principal, ele assina tópicos sensor_statee camera_statesalva o conteúdo de mensagens em variáveis ​​globais em funções de retorno de chamada. Ele também está inscrito no tópico terminator, cujo retorno de chamada redefine o sinalizador is_running, interrompendo o loop principal. Além disso, antes do início do ciclo, o nó anuncia as interfaces para os serviços do nó servo e aguarda alguns segundos para que os outros nós sejam inicializados. Depois que o loop principal termina, esse nó chama os serviçosterminate_{switch, camera, sensor, servo}, desativando todos os outros nós e, em seguida, desativando-o. Ou seja, quando o interruptor é desligado, todos os cinco nós concluem a operação.

Mudar para o ROS me forçou a mudar bastante a estrutura do programa. Por exemplo, anteriormente era possível alterar a velocidade da roda com alta frequência, e isso funcionava bem, mas o serviço ROS trabalha em uma ordem de magnitude mais lenta, então tive que reescrever o código para que o serviço fosse chamado apenas quando a velocidade realmente mudar (no "modo lento").

O ROS também permite executar comodamente todos os nós do robô. Para fazer isso, você precisa escrever um arquivo de inicialização .launch listando todos os nós e outros atributos do robô no formato xml e, em seguida, execute o comando:

roslaunch beggar_bot robot.launch

ROS x pthread

Agora, finalmente, você pode comparar a velocidade da versão ROS e da versão pthread. Eu faço da seguinte maneira: o thread / nó responsável por trabalhar com a câmera considera seu FPS (como o elemento mais lento), desde que todo o resto também funcione. Para a versão pthread, eu sempre obtive o FPS 9.99 ou mais; para a versão ROS, ficou em torno de 8.3. De fato, isso é suficiente para um brinquedo assim, mas a sobrecarga é bastante perceptível.

Comportamento do robô


A idéia é bastante simples: se o robô vê uma pessoa, ele deve dirigir-se a ele e apertar a lata. Agitar a jarra é bastante simples e divertido, mas primeiro você precisa chegar à pessoa.

Existe uma função follow_faceque, se houver uma face no quadro, gira o chassi e a cabeça do robô em sua direção (somente a face mais próxima do centro é levada em consideração). Isso é necessário para que o robô mantenha sempre o curso de uma pessoa, se ela estiver na moldura, e também olhe diretamente na cara quando ele sacode uma jarra.

A camera_statemesma variável é usada para sincronizar esta função com o tópico .face_processed, como na versão com fluxos. A ideia é a mesma - queremos processar dados apenas uma vez, porque o robô está em constante movimento. A função primeiro espera até que o retorno de chamada do tópico com as detecções diminua o sinalizador de que o último quadro foi processado. Enquanto ela espera, ela liga constantemente ros::spinOnce()para receber novas mensagens (em geral, isso deve ser feito sempre que o programa espera novos dados). Se houver uma face no quadro, os ângulos são calculados, o que precisa girar a plataforma e a cabeça - isso pode ser feito conhecendo as coordenadas relativas do centro da face e o campo de visão da câmera na horizontal e na vertical. Depois disso, você pode ligar para o serviço servo_head_platforme mover o robô.

Há um ponto sutil: as informações sobre a posição do rosto ficam atrás do movimento real do rosto e podem ficar atrás dos movimentos do próprio robô. Portanto, o robô pode superestimar o ângulo de rotação, pelo que a cabeça começa a se mover para frente e para trás com amplitude crescente. Para evitar isso, faço atrasos após a movimentação (300 ms) e também pulo um quadro após a movimentação. Para a mesma finalidade, os ângulos de rotação do chassi e da cabeça são multiplicados por um fator de 0,8 (os componentes P do controlador PID fazem sentido ).

Funçãofollow_faceretorna o status de uma pessoa. Uma pessoa pode: estar ausente, estar perto o suficiente do centro, estar muito longe do robô; outra opção - quando ligamos o robô e não sabemos o que aconteceu com o rosto (no processo de busca); ainda há um caso raro em que a cabeça está na borda, e é por isso que é impossível virar o rosto.

Uma coisa bastante simples acontece no loop principal:

  1. Ligue follow_faceaté a pessoa ter um determinado status (qualquer um, exceto "no processo de pesquisa"). No final desta etapa, o robô olhará diretamente para o rosto.
  2. Se o rosto for encontrado e estiver próximo:
    1. Agite a lata;
    2. Encontre o rosto novamente;
    3. Se o rosto estiver no lugar, faça uma boa expressão com as sobrancelhas e agite o frasco novamente;
    4. Se o rosto desapareceu, faça uma expressão de raiva com as sobrancelhas;
    5. Vire-se, vá para o início do ciclo.

  3. Se não houver pessoa (ou estiver longe) - navegue pela sala:
    1. Se houver longe de obstáculos de ambos os lados, siga em frente (se o rosto foi encontrado, mas se mostrou muito distante, o robô irá para a pessoa);
    2. Se houver obstáculos próximos dos dois lados, faça uma curva aleatória no intervalo [90,180][180,90];
    3. Se o obstáculo estiver apenas de um lado, gire na direção oposta em um ângulo aleatório [0,90];
    4. Se o movimento para a frente continuar por muito tempo (possivelmente travado), recue um pouco e faça uma curva aleatória no intervalo [90,180][180,90];


Esse algoritmo não afirma ser uma forte inteligência artificial; no entanto, o comportamento aleatório e um amplo amortecedor permitem que o robô saia de quase qualquer posição mais cedo ou mais tarde.

Conclusão


Apesar de sua aparente simplicidade, este projeto abrange muitos tópicos não triviais: trabalho com sensores analógicos, trabalho com PWM, visão computacional, coordenação de tarefas assíncronas. Além disso, é incrivelmente divertido. Provavelmente, além disso, farei algo mais significativo, mas mais com um viés no aprendizado profundo.

Como um bônus - a galeria:








All Articles