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.