6.1. Por que arrays

A classe Image é a ferramenta certa para trabalho com pixels porque cada método dela opera diretamente sobre o buffer de pixels nativo da câmera em uma única chamada rápida. A maior parte do que a aplicação faz com um quadro – limiarização, busca de blobs, detecção de AprilTag, filtros de borda – já reside ali.

O que a biblioteca de imagem não expõe é o restante do 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 interno retorna – uma coluna de histograma, uma mistura personalizada de dois quadros, uma transformação por pixel que o catálogo não cobre,

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

  • matemática de processamento de sinais – 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 quer como entrada.

Todos esses querem a mesma forma: um buffer de números com uma operação aplicada a cada elemento. Um laço for em Python é a forma óbvia de escrevê-lo:

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

O laço funciona. Ele também é lento. Python é uma linguagem interpretada, e cada iteração de um laço Python carrega o custo de executar o interpretador uma vez: procurar samples, ler o elemento i, multiplicar, escrever de volta, avançar o contador do laço, verificar a condição do laço. Em um buffer de mil amostras de sensor esses custos do interpretador somam dezenas de milissegundos para o que é fundamentalmente uma operação rápida.

Esse overhead morde toda vez que um script alcança um buffer. Um quadro QVGA em escala de cinza tem 76.800 pixels; um acelerômetro a 100 Hz entrega cem amostras de três eixos por segundo; um microfone preenche um buffer de 1024 amostras a cada 64 ms. Um laço for em Python puro sobre qualquer um deles transforma uma tarefa que deveria levar alguns microssegundos em uma que leva dezenas de milissegundos – e cerca de dez vezes mais ainda em um buffer do tamanho de uma imagem.

6.1.1. Funções de biblioteca são mais rápidas que laços

A solução é expressar a operação como uma única chamada de função contra o buffer inteiro, em vez de um laço Python sobre seus elementos. O numpy é exatamente isso: uma biblioteca de matemática de arrays onde 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 dentro de uma única chamada – a mesma aritmética que o laço fazia, sem o custo do interpretador por iteração. A mesma multiplicação de 1000 elementos que levava dezenas de milissegundos como um laço Python leva dezenas de microssegundos como uma chamada numpy.

Este é o acordo que o numpy oferece em toda a linha: sum, mean, sin, exp, multiplicação de matrizes, primitivas de processamento de sinais – cada uma é uma única função de biblioteca que opera sobre um buffer inteiro de uma vez. A troca é que os dados têm que viver no tipo de array do numpy e a operação tem que ser expressa contra esse array, não contra seus elementos um de cada vez.

6.1.2. Por que uma list 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 lê ainda tem que olhar cada slot para descobrir o que há nele e extrair o valor antes que qualquer aritmética aconteça. Esse overhead por slot é exatamente o custo que o laço Python paga. Listas são a escolha errada para matemática de arrays rápida.

6.1.3. Por que bytearray também não basta

Um bytearray é a forma certa – um buffer tipado, um byte por elemento, tudo em um único bloco contíguo. É o que a maioria das APIs de periféricos orientadas a bytes devolve. O que lhe falta é a matemática. bytearray * 2 repete o buffer em vez de dobrar cada valor, e não há significado sensato para bytearray + bytearray elemento a elemento.

A estrutura de dados que combina um buffer tipado com matemática elemento a elemento é o ndarray. O que está dentro da caixa e como cada campo molda o comportamento de caminho rápido são as fundações sobre as quais o resto deste capítulo se apoia.