6.18. Imagens e ndarrays

A classe Image é a superfície rápida para trabalho com pixels nativos da câmara: cada método nela opera diretamente no buffer de fotograma no formato de pixel nativo da câmara. O módulo numpy é a superfície numérica genérica para tudo o resto. Dois métodos fazem a ponte entre eles:

Em conjunto, permitem que uma aplicação capture um fotograma, passe-o ao numpy para uma transformação personalizada e, depois, coloque o resultado de volta numa imagem para apresentar, guardar ou injetar no resto da biblioteca de imagem.

6.18.1. Imagem para ndarray

to_ndarray() aloca um novo ndarray e copia os dados de pixel da imagem para ele (com o mapeamento de dtype indicado abaixo). Nunca é uma vista sobre o buffer de fotograma da imagem – o array numpy possui sempre os seus próprios bytes. A assinatura é to_ndarray(dtype, *, buffer=None), e a forma da saída depende do formato da imagem:

  • GRAYSCALE – array 2-D, forma (height, width).

  • RGB565 – array 3-D, forma (height, width, 3), planos na ordem R/G/B.

O argumento dtype controla como cada valor de pixel de 8 bits v é mapeado:

dtype

elemento

mapeamento para um valor de pixel de 8 bits v

'B'

uint8

v (bruto)

'b'

int8

v - 128 (recentrado em torno de zero)

'f'

float32

float(v) (0.0 … 255.0)

Exemplo – ver um fotograma em escala de cinzentos como uma matriz uint8

import csi
from ulab import numpy as np

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.GRAYSCALE)
csi0.framesize(csi.QVGA)

img = csi0.snapshot()
a   = img.to_ndarray('B')        # shape (240, 320), dtype=uint8

print(a.shape, a.dtype)
print("mean brightness:", np.mean(a))

A palavra-chave buffer= permite à aplicação reutilizar um bytearray já alocado, evitando assim que a câmara tenha de alocar um novo em cada fotograma:

buf = bytearray(320 * 240)
while True:
    img = csi0.snapshot()
    a   = img.to_ndarray('B', buffer=buf)
    # ... process a ...

6.18.2. ndarray para imagem

No sentido inverso, passe o ndarray como primeiro argumento para image.Image. O construtor aloca um novo buffer de imagem e copia os valores do array para ele, limitados e arredondados para 0..255

image.Image(arr, *, buffer=None, copy_to_fb=False)

O construtor infere a geometria e o formato de pixel a partir da forma do array:

  • forma (h, w) – imagem GRAYSCALE.

  • forma (h, w, 3) – imagem RGB565.

O ndarray deve ter dtype float; o construtor apenas suporta este caso atualmente. Os valores são arredondados e limitados ao intervalo 0..255.

buffer= permite à aplicação fornecer um bytearray já alocado para a imagem resultante. copy_to_fb=True escreve o resultado no buffer de fotograma da câmara, o que é a escolha certa quando o resultado deve aparecer na pré-visualização da IDE.

6.18.3. Viagem de ida e volta

import csi
import image
from ulab import numpy as np

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.GRAYSCALE)
csi0.framesize(csi.QVGA)

img = csi0.snapshot()
a   = img.to_ndarray('f')                  # work in float space

a = 255.0 * (a / 255.0) ** 0.5             # gamma correction

out = image.Image(a, copy_to_fb=True)      # back to an image

6.18.4. Quando usar a ponte

Esta ponte é a resposta certa quando a aplicação precisa de uma operação numérica genérica que os métodos integrados do image não fornecem – filtros personalizados, combinações personalizadas, não-linearidades invulgares – ou quando os dados de pixel precisam de ser combinados com dados que não são de imagem (eixos IMU, amostras de áudio) num único cálculo.

Não é a resposta certa para o processamento de pixels de alto débito que a classe Image já cobre. Os métodos integrados operam diretamente no buffer de fotograma no formato de pixel nativo da câmara e são muito mais rápidos do que a expressão equivalente em numpy. Recorra à ponte apenas para as operações que a biblioteca de imagem ainda não fornece.