6.18. Imagens e ndarrays

A classe Image é a superfície rápida para o trabalho com pixels nativos da câmera: todos os seus métodos operam diretamente no frame buffer no formato de pixel nativo da câmera. O numpy é a superfície numérica genérica para todo o resto. Dois métodos fazem a ponte entre eles:

Juntos, eles permitem que uma aplicação capture um quadro, o entregue ao numpy para uma transformação personalizada e, em seguida, coloque o resultado de volta em uma imagem para exibir, salvar ou realimentar o restante da biblioteca de imagens.

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 abaixo). Nunca é uma visualização do frame buffer da imagem – o array numpy sempre é dono de seus próprios bytes. A assinatura é to_ndarray(dtype, *, buffer=None), e o formato da saída depende do formato da imagem:

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

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

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

dtype

elemento

mapeamento para um valor de pixel v de 8 bits

'B'

uint8

v (bruto)

'b'

int8

v - 128 (recentralizado em torno de zero)

'f'

float32

float(v) (0.0 … 255.0)

Exemplo – visualizar um quadro em escala de cinza 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 que a aplicação reutilize um bytearray que já alocou, de modo que a câmera não precise alocar um novo a cada quadro:

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, restringidos 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 do formato do array:

  • formato (h, w) – imagem GRAYSCALE.

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

O ndarray deve ter dtype float; o construtor só oferece suporte a esse caso atualmente. Os valores são arredondados e restringidos ao intervalo 0..255.

buffer= permite que a aplicação forneça um bytearray que já alocou para a imagem resultante. copy_to_fb=True grava o resultado no frame buffer da câmera, o que é a escolha certa quando o resultado deve aparecer na pré-visualização do IDE.

6.18.3. 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 fazer a ponte

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

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