Estou construindo meu próprio sistema operacional Linux integrado para Raspberry PI3 usando o Buildroot. Este sistema operacional será usado para lidar com vários aplicativos, um deles realiza a detecção de objetos com base no OpenCV (v3.3.0).
Comecei com Raspbian Jessy + Python, mas descobri que leva muito tempo para executar um exemplo simples, então decidi projetar meu próprio RTOS com recursos otimizados + desenvolvimento C++ em vez de Python.
Achei que com essas otimizações os 4 núcleos de RPI + 1GB de RAM dariam conta de tais aplicações. O problema é que, mesmo com essas coisas, os programas mais simples de Visão Computacional levam muito tempo.
Comparação entre PC e Raspberry PI3
Este é um programa simples que escrevi para ter uma ideia da ordem de grandeza do tempo de execução de cada parte do programa.
#include <stdio.h>
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <time.h> /* clock_t, clock, CLOCKS_PER_SEC */
using namespace cv;
using namespace std;
int main()
{
setUseOptimized(true);
clock_t t_access, t_proc, t_save, t_total;
// Access time.
t_access = clock();
Mat img0 = imread("img0.jpg", IMREAD_COLOR);// takes ~90ms
t_access = clock() - t_access;
// Processing time
t_proc = clock();
cvtColor(img0, img0, CV_BGR2GRAY);
blur(img0, img0, Size(9,9));// takes ~18ms
t_proc = clock() - t_proc;
// Saving time
t_save = clock();
imwrite("img1.jpg", img0);
t_save = clock() - t_save;
t_total = t_access + t_proc + t_save;
//printf("CLOCKS_PER_SEC = %d\n\n", CLOCKS_PER_SEC);
printf("(TEST 0) Total execution time\t %d cycles \t= %f ms!\n", t_total,((float)t_total)*1000./CLOCKS_PER_SEC);
printf("---->> Accessing in\t %d cycles \t= %f ms.\n", t_access,((float)t_access)*1000./CLOCKS_PER_SEC);
printf("---->> Processing in\t %d cycles \t= %f ms.\n", t_proc,((float)t_proc)*1000./CLOCKS_PER_SEC);
printf("---->> Saving in\t %d cycles \t= %f ms.\n", t_save,((float)t_save)*1000./CLOCKS_PER_SEC);
return 0;
}
Resultados da execução em um PC i7
Resultados da execução no Raspberry PI (SO gerado a partir do Buildroot)
Como você pode ver, há uma grande diferença. O que eu preciso é otimizar cada detalhe para que esta etapa de processamento de exemplo ocorra "quase" em tempo real em um tempo máximo de 15ms .
Minha dúvida é sobre:
- Como posso otimizar meu sistema operacional para que ele possa lidar com aplicativos de cálculos intensivos e como posso controlar as prioridades de cada parte?
- Como posso usar totalmente os 4 núcleos do RPI3 para atender aos requisitos?
- Existe alguma outra possibilidade em vez do OpenCV?
- Devo usar C em vez de C++?
- Quaisquer melhorias de hardware que você recomenda?
Em ordem:
Para otimização geral, não há muito que você possa fazer no lado do sistema operacional além das coisas normais, como garantir que você tenha apenas o que realmente precisa rodando em segundo plano. No Pi original, você poderia acelerar
memmove()
e funções semelhantes usandoLD_PRELOAD
uma biblioteca chamada 'cofi' que fornecia versões otimizadas de montagem dessas funções, mas não tenho certeza se isso ajudará em um Pi 3.Para priorização, isso é realmente algo para procurar nas páginas de manual, mas você geralmente não pode fazer isso a menos que paralelize as coisas (no seu caso, parece que a solução óbvia é executar cada etapa à medida que é processada e usar o IPC (provavelmente memória compartilhada por motivos de desempenho) para mover os dados entre eles).
Na nota dos resultados que você citou de seu programa de teste, observe em particular que as etapas de processamento e salvamento são cerca de 10 vezes mais lentas no Pi, enquanto a etapa de acesso é apenas cerca de 5 vezes mais lenta e esses números correspondem a um estimativa aproximada do que eu esperaria ao comparar um Pi 3 a um PC genérico com menos de um ano de idade. A CPU no Pi é quase certamente significativamente mais lenta do que aquela em que você executou o teste de PC (e se você não paralelizou as coisas, a lacuna aumenta ainda mais, já que a maioria das CPUs x86 modernas podem executar um único núcleo por si só em carga total muito mais rápido do que eles podem executar todos os seus núcleos com carga total), e isso terá um impacto. O ARM ISA também é significativamente diferente do x86 ISA (o ARM tende a fazer menos por ciclo em comparação com o x86, mas não
Também não sei qual câmera você está usando, mas espero que você consiga tempos melhores cortando a resolução das imagens que está processando e provavelmente pode reduzir o tempo de aquisição se evitar usar formatos compactados (e não usar compactação com perdas significa que a resolução não importará tanto).
Paralelização em seu próprio código. Você só precisa ter certeza de que o SMP está ativado em seu kernel (e se estiver usando a configuração oficial da Fundação RPi, deve estar) e tente executar as coisas em paralelo. Não tenho certeza de quanto o OpenCV faz para paralelizar as coisas em si, mas você também pode querer olhar para o OpenMP (ele fornece uma maneira razoavelmente fácil de paralelizar iterações em loops que não são interdependentes).
Pode haver, mas todo mundo está padronizado no OpenCV, então eu sugiro usá-lo (você terá mais facilidade em obter ajuda técnica para implementar as coisas porque todo mundo o usa).
Isso depende de como você está usando as coisas. Embora seja muito mais fácil escrever código lento em C++ do que em C, não é mais difícil escrever código rápido em qualquer linguagem. Muitas das técnicas de otimização são bastante semelhantes em ambas as linguagens (por exemplo, pré-alocar tudo na inicialização para que você não chame
malloc()
em seções críticas ou evite chamarstat()
). No caso de C++ especificamente, porém, evitestd::string
como a praga, ele chamamalloc()
em todo o lugar e, como resultado, é incrivelmente lento (eu vi conversões que mudamstd::string
para strings no estilo C melhoram o desempenho em mais de 40% em alguns casos ).Supondo que você esteja tentando manter os custos de hardware baixos e com restrição de espaço (portanto, a escolha do Raspberry Pi), não há realmente nenhum em que eu possa pensar. O Pi (em todas as suas iterações) usa um SoC que é adequado exclusivamente para o trabalho de visão computacional nessa faixa de preço. Se você estiver disposto a usar algo um pouco maior e um pouco mais caro, posso sugerir uma placa NVIDIA Jetson (eles usam um Tegra SoC que tem uma GPU equivalente a Quadro integrada com 192 núcleos CUDA, então provavelmente poderia executar seu processamento carga de trabalho muito mais rápida), mas fazer o Buildroot funcionar é significativamente mais complicado do que em um Pi.
Edições em resposta aos comentários:
A paralelização no nível do processo não é a mesma coisa que multithreading, é drasticamente diferente (a maior diferença está em como os recursos são compartilhados, por padrão, os threads compartilham tudo, os processos não compartilham nada). Em geral, quando há muito processamento envolvido, é (geralmente) melhor usar a paralelização baseada em processo, pois é mais fácil escrever código eficiente sem ter que se preocupar com a segurança do thread.
No que diz respeito às opções, as duas que você mencionou podem ter um grande impacto no desempenho do sistema, mas ambas acabam sendo compensações entre taxa de transferência e latência. O modelo de preempção controla como as coisas em execução no modo kernel (como syscalls) podem ser reagendadas. As três opções são:
A frequência do temporizador, em contraste, é muito mais fácil de explicar. Ele controla o período de tempo mais longo que algo pode ser executado ininterruptamente se houver algo esperando para ser executado. Valores mais altos resultam em um período de tempo mais curto (menor latência e menor taxa de transferência), valores mais baixos, um período mais longo (maior latência e maior taxa de transferência). Para um começo geral, sugiro definir o modelo de preempção como voluntário e a frequência do temporizador para 300 Hz e, em seguida, começar a experimentar a alteração da frequência do temporizador primeiro (já que isso geralmente terá um impacto mais visível).
Quanto ao Movidius NCS, vale a pena ou não, depende de quantos dados você precisa para trabalhar, porque será largura de banda limitada pela conexão USB (o Pi possui apenas um único controlador USB 2.0, portanto, você não está limitado apenas a menos de um décimo da largura de banda para a qual o Movidius foi projetado, você também deve compartilhar o barramento com pelo menos o adaptador Ethernet, o que prejudicará sua latência e sua taxa de transferência). Se você estiver fazendo apenas quadros únicos de 1920 x 1080 com cores de 32 bits a uma taxa baixa, pode ser viável, mas se precisar fazer o processamento de streaming desse mesmo vídeo em taxas de quadros completas, provavelmente terá latência questões. Se você optar por usar um, certifique-se de obter um hub alimentado para ele (caso contrário, você pode ter problemas ao tentar extrair mais energia do que o Pi pode fornecer).