As bibliotecas NumpY, voltada para computação numérica, e OpenCV (Open Source Computer Vision Library), que possui vários módulos de processamento de imagens, são muito úteis para trabalhar com imagens. Uma imagem é feita de pequenos elementos quadrados chamados pixels. Cada imagem tem três propriedades principais:
- Tamanho, dada por altura e largura, gerlamente é representado em centímetros, polegadas ou pixels;
- Espaço de cores, como RGB e HSV;
- Canal, um atributo do espaço de cores.
Assim, o espaço de cores RGB tem três tipos de cores (ou atributos) conhecidos como Vermelho, Verde e Azul (daí o nome RGB). Uma imagem RGB tem três canais de cores: canal vermelho, canal verde e canal azul. Já uma imagem em tons de cinza tem apenas um canal.
As cores de uma imagem são indicadas por seus valores de pixel. Um pixel pode ter apenas uma cor, mas pode ser mesclado para criar várias cores. Em uma imagem em tons de cinza (“grayscale”), um valor de pixel tem apenas um único número variando de 0 a 255 (ambos inclusive). O valor de pixel 0 representa preto e o valor de pixel 255 representa branco.
O mesmo vale para cada uma das cores individualmente. Por exemplo, um pixel amarelo é representado matricialmente da seguinte forma: [255, 255, 0] (Red, Green e Blue, nessa ordem). As cores são formadas pela soma dessas cores primárias.
Em python, é possível trabalhar com imagens em formato usando a biblioteca NumPy. Seu principal objeto é o vetor n-dimensional, ou ndarray. Um vetor n-dimensional também é conhecido pelo nome tensor. Um tensor de segunda ordem (bidimensional) é uma matriz. Ela é representada aqui pelo número de linhas seguido pelo número de colunas e de canais/camadas, por exemplo: (640,480,3) = (linhas, colunas, canais).
Uma única imagem RGB pode ser representada usando um array NumPy tridimensional (3D). Para uma imagem de três colunas e duas linhas com quadrados pretos e brancos alternados, a representação de uma imagem em tons de cinza seria: [[0,255,0],[255,0,255]]. No caso de uma imagem RGB, cada número é substituído por um outro vetor, com um número para cada cor RGB. Observe o exemplo descrito na figura a seguir:
No OpenCV, a sequência BGR é usada em vez de RGB. Isso significa que o primeiro canal é azul, o segundo canal é verde e o terceiro canal é vermelho. As imagens geradas acima podem ser criadas em python através do seguinte código:
import numpy as np import cv2 img = np.array([ [0,255,0], [255,0,255] ]) print(img) cv2.imwrite('grayscale.png', img) img = np.array([ [[0,0,0],[255,255,255],[0,0,0]], [[255,255,255],[0,0,0],[255,255,255]] ]) print(img) cv2.imwrite('rgb.png', img) img = np.array([ [[255,0,0],[255,255,255],[0,0,255]], [[255,255,255],[0,255,0],[255,255,255]] ]) print(img) cv2.imwrite('rgb_colors.png', img)
No entanto, se quiser converter de BGR para RGB é bem simples: “img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)”. Essa mesma flag pode indicar a conversão para escala de cinza (IMREAD_GRAYSCALE), por exemplo. Para selecionar um canal referente a uma das cores, pode-se usar a seguinte estrutura:
b = img[:,:,0] # canal azul g = img[:,:,1] # canal verde r = img[:,:,2] # canal vermelho
Isso funciona para BGR, ou seja, RGB precisa inverter os números dos canais azul e vermelho, seguindo a ordem da respectiva sigla.
Também é possível criar um array com mais de uma imagem dentro. A forma mais comum de um lote com imagens ser representada é conhecida como “Channels-last”. Esse é padrão no TensorFlow (Keras) e no OpenCV. Nela, o eixo do canal de cores no final. Por exemplo, para 4 imagens RGB com 2 pixels de altura e 3 de largura seria (4, 2, 3, 3).
Implementação de máscara
Pode-se utilizar máscaras para realizar operações somente em uma parte da imagem – por exemplo, para desconsiderar valores inválidos ou selecionar uma região. O tipo mais comum de máscara é a binária, onde uma parte é formada de pixels referentes aos quais devem ser considerados (valor 1 ou True) e outra parte não (valor 0 ou False) na imagem a ser trabalhada.
A combinação da máscara com o array original gera uma matriz mascarada (“Masked Array”). O módulo numpy.ma trabalha dessa forma, mas com um pensamento oposto:
- elemento recebe False/0 – sem aplicação de máscara (mantém dado);
- elemento recebe True/1 – usa máscara (desconsidera dado mascarado).
Para inverter o efeito desse comportamento, basta colocar o sinal de “til” na frente da máscara.
No exemplo a seguir, são chamadas quatro funções:
- create_img – cria uma imagem em escala de cinza com valores randômicos, no caso, de nove linhas e quinze colunas;
- create_circular_mask – cria uma máscara circular de centro coincidente com a imagem original e raio calculado com base no centro e nas medidas da imagem;
- apply_mask – aplica a máscara na imagem (de forma invertida, devido ao sinal de negação) para que sejam selecionados somente os pixeis internos ao círculo desenhado pela máscara – os pixels de fora são substituídos por “–” que representam dados inválidos;
- view_img – faz uso do método “filled” para preencher os valores inválidos da máscara com zero, de modo que os respectivos pixels apareçam pretos (quando o opencv abrir uma janela com a imagem, basta teclar qualquer tecla para que ela feche e libere o terminal).
#!/usr/bin/env python3.7.13 # -*- Coding: UTF-8 -*- import numpy as np import cv2 def create_image(vmax, nrows, ncols): np.random.seed(0) # Fix seed img = np.random.randint(vmax, size=(nrows, ncols)) return img def create_circular_mask(h, w, center, radius): # use the middle of the image if center is None: center = (int(w/2), int(h/2)) # use the smallest distance between the center and image walls if radius is None: radius = min(center[0], center[1], w-center[0], h-center[1]) Y, X = np.ogrid[:h, :w] dist_from_center = np.sqrt((X - center[0])**2 + (Y-center[1])**2) mask = dist_from_center <= radius return mask def apply_mask(img, mask): # How many Trues and Falses? n_true = np.count_nonzero(mask) n_false = np.size(mask) - np.count_nonzero(mask) print(f'n_True = {n_true}; n_False = {n_false}') # Apply mask to image masked = np.ma.masked_array(img, ~mask) return masked, n_true, n_false def view_img(title, img): # Fill invalid values with 0 to plot black pixels img = img.filled(fill_value=0) cv2.imshow(title, img) cv2.waitKey(0) # Generate random grayscale image as numpy array img = create_image(255, 9, 12) print(img) mask = create_circular_mask(img.shape[0], img.shape[1], None, None) print(mask) masked, n_true, n_false = apply_mask(img, mask) print(masked) print(f'Original_sum = {img.sum()}; Masked_sum = {masked.sum()}') view_img('Grayscale image with mask', masked)
Dessa forma, as entradas associadas às posições marcadas pela máscara como inválidas não são utilizadas em cálculos. A última linha mostra o somatório de todos os elementos a imagem original e na mascarada, e os totais são diferentes.
Você também pode trabalhar e visualizar uma matriz mascarada usando “flexible-type array”. O método “.toflex()” retorna um novo ndarray de tipo flexível com dois campos: o primeiro elemento contendo um valor, o segundo elemento contendo a máscara booleana correspondente – (49, False) por exemplo.
Outra forma de implementar uma máscara é através do método “bitwise_and” do OpenCV: “cv2.bitwise_and(img,img,mask = mask)”. Para contabilizar o número de pixels válidos (diferentes de zero), pode-se usar o método “countNonZero”.