6.1. Porquê arrays

A classe Image é a ferramenta certa para trabalho com pixels porque todos os seus métodos operam diretamente sobre o buffer de pixels nativo da câmara numa única chamada rápida. A maior parte do que a aplicação faz a um fotograma – limiarização, deteção de manchas, deteção de AprilTag, filtros de aresta – já se encontra disponível.

O que a biblioteca de imagem não expõe é o restante trabalho numérico com que uma aplicação OpenMV se depara:

  • buffers de sensor que não são pixels – amostras de ADC, eixos de uma IMU (unidade de medição inercial), áudio de microfone,

  • números derivados da imagem que nenhum método integrado devolve – uma coluna de histograma, uma mistura personalizada de dois fotogramas, uma transformação por pixel que o catálogo não contempla,

  • álgebra linear simples – a matriz de calibração que retifica a lente, a rotação que funde a IMU,

  • matemática de processamento de sinal – o conteúdo de frequência de um buffer de vibração, suavização aplicada à saída de um sensor, um vetor de características que um classificador recebe como input.

Todos estes casos necessitam da mesma forma: um buffer de números com uma operação aplicada a cada elemento. Um ciclo for em Python é a forma óbvia de escrever isso:

for i in range(len(samples)):
    samples[i] = samples[i] * cal

O ciclo funciona. Mas é lento. Python é uma linguagem interpretada, e cada iteração de um ciclo Python acarreta o custo de executar o interpretador uma vez: procurar samples, ler o elemento i, multiplicar, escrever de volta, avançar o contador do ciclo, verificar a condição do ciclo. Num buffer de mil amostras de sensor, esses custos de interpretação acumulam-se em dezenas de milissegundos para o que é fundamentalmente uma operação rápida.

Esse overhead afeta sempre que um script acede a um buffer. Um fotograma QVGA em escala de cinzentos tem 76 800 pixels; um acelerómetro a 100 Hz fornece cem amostras de três eixos por segundo; um microfone preenche um buffer de 1024 amostras a cada 64 ms. Um ciclo for em Python puro sobre qualquer destes transforma uma tarefa que deveria demorar alguns microssegundos numa que demora dezenas de milissegundos – e aproximadamente dez vezes mais num buffer do tamanho de uma imagem.

6.1.1. As funções de biblioteca são mais rápidas do que ciclos

A solução é expressar a operação como uma única chamada de função sobre todo o buffer, em vez de um ciclo Python sobre os seus elementos. O numpy é exatamente isso: uma biblioteca de aritmética sobre arrays em que cada operação é uma função já otimizada que percorre o buffer uma vez do início ao fim. np.multiply(samples, cal) multiplica cada elemento de samples por cal numa única chamada – a mesma aritmética que o ciclo fazia, sem o custo do interpretador por iteração. A mesma multiplicação de 1000 elementos que demorava dezenas de milissegundos como ciclo Python demora dezenas de microssegundos como chamada numpy.

Este é o acordo que o numpy oferece em geral: soma, média, seno, exponencial, multiplicação matricial, primitivas de processamento de sinal – cada uma é uma única função de biblioteca que opera sobre um buffer inteiro de uma vez. A contrapartida é que os dados têm de residir no tipo array do numpy e a operação tem de ser expressa contra esse array, e não contra os seus elementos um de cada vez.

6.1.2. Porquê uma lista não serve

Uma list Python não pode substituir. Uma lista pode conter qualquer mistura de objetos – inteiros, floats, strings, outras listas – e uma função de biblioteca que a leia ainda tem de verificar cada slot para saber o que contém e extrair o valor antes de qualquer aritmética ocorrer. Esse overhead por slot é exatamente o custo que o ciclo Python paga. As listas são inadequadas para aritmética de arrays rápida.

6.1.3. Porquê o bytearray também não é suficiente

Um bytearray tem a forma correta – um buffer tipado, um byte por elemento, tudo num bloco contíguo. É o que a maioria das APIs de periféricos orientadas a bytes devolve. O que lhe falta é a aritmética. bytearray * 2 repete o buffer em vez de duplicar cada valor, e não existe um significado sensato para bytearray + bytearray elemento a elemento.

A estrutura de dados que combina um buffer tipado com aritmética por elemento é o ndarray. O que está dentro da caixa e como cada campo molda o comportamento no caminho rápido são os fundamentos sobre os quais repousa o resto deste capítulo.