14. Tipo NPImagem¶
Vimos que o tipo ndarray do módulo NumPy é muito utilizado em computação científica. Nessa aula vamos continuar a explorar essa estrutura para treinar o uso de fatias e operações com arrays sem o uso de laços.
14.1. Objetivos de aprendizagem¶
Realize seus estudos para que, ao final dessa aula, você seja capaz de:
- utilizar fatias de arrays para evitar o uso de laços;
- explicar como utilizar
ndarray
para representar uma imagem; - criar a classe
NPImagem
baseada emndarray
; - usar objetos do tipo
NPImagem
em seus programas.
14.2. Introdução¶
Uma grande vantagem de usar arrays do NumPy é a facilidade de manipular essas estruturas n-dimensionais sem usar laços. Esse é mais um poderoso exemplo de abstração, que nos permite pensar na informação que precisamos processar sem se preocupar com a implementação interna.
Para realçar esse ponto, considere o seguinte trecho de código abaixo que cria um array \(4\times5\) e preenche uma região definida pelo canto superior-esquerdo (1,1) até o canto inferior-direito (3,4) com o valor -1.
Como fatias são vistas do array, podemos usar fatias de arrays para modificar regiões como:
import numpy as np
li = list(range(20))
ar = np.array(li).reshape(4,5)
print(ar)
# carrega a região definida pelo canto superior-esquerdo (1,1)
# e canto inferior-direito (4,4) com o valor -1
s, e = 1, 1
i, d = 3, 4
v = -1
for lin in range(s, i):
ar[lin, e:d] = v # fatia e:d evita o laço que varre as colunas
print(ar)
e mais ainda, podemos combinar as fatias para as linhas e colunas como:
import numpy as np
li = list(range(20))
ar = np.array(li).reshape(4,5)
print(ar)
# carrega a região definida pelo canto superior-esquerdo (1,1)
# e canto inferior-direito (4,4) com o valor -1
s, e = 1, 1
i, d = 3, 4
v = -1
# fatia [s:i,e:d] define um retângulo que recebe o valor v
ar[ s:i, e:d ] = v
print(ar)
Esse código, sem laços, tende a ser mais eficaz, fácil de escrever e, depois que você se acostumar com a notação, mais fácil de ler. Edite o código na janela do Trinket para verificar se os resultados são os mesmos.
A seguir vamos treinar o uso de fatias para evitar laços usando a classe NPImagem para trabalhar com imagens digitais.
14.3. Imagem Digital¶
Uma imagem digital ou simplesmente imagem é basicamente uma matriz (um objeto com duas dimensões), com, digamos, altura (= height = número de linhas) e largura (= width = número de colunas). Cada elemento da matriz é chamada de pixel (= picture element), que possui uma “cor”.
Na sua forma mais básica, a cor de um pixel pode ser representado por 1 bit (= simplificação de dígito binário, em inglês binary digit). O bit 1 indica que o pixel está aceso ou é “branco”. O bit 0 indica que o pixel está apagado ou é “preto”. Imagens binárias são aquelas em que cada pixel é branco ou preto.
No entanto, os valores, ou níveis, 0 e 1 são insuficientes para representarmos o que costumamos chamar de imagens em preto e branco, pois estas possuem vários níveis ou tons de cinza. Uma forma comum de representar uma imagem com vários tons de cinza é reservando um byte para cada pixel. Um byte consiste de 8 bits representando um valor entre 0 e 255, ou seja, com um byte podemos representar até 256 tons de cinza.
Chamamos as imagens em que cada pixel pode ter vários tons de cinza de imagens com níveis de cinza (grayscale).
Uma imagem com níveis de cinza nos permite ver as variações de luminosidade da cena. Já uma imagem colorida requer ainda mais informação para cada pixel. Baseado no sentido da visão humana, que é tricromática, a representação de imagens mais comum é obtida decompondo uma cor nas componentes básicas vermelho (red), verde (green), e azul (blue) ou RGB. Assim, usando um total de três bytes: um byte para níveis de vermelho; um byte para níveis de verde e um byte para níveis de azul, podemos representar aproximadamente todo o espectro visível de cores.
14.4. A classe NPImagem¶
A classe NPImagem
deve representar imagens digitais por meio de ndarrays. Para isso, um objeto da classe NPImagem deve possuir um atributo de nome data
do tipo numpy.ndarray que é utilizado internamente nas operações com imagens dessa classe.
Seja img
um objeto da classe NPImagem. Então img
deve se comportar da seguinte forma:
- Criação usando
img = NPImagem( (nlins, ncols), val)
. Um objeto NPImagem é criado ao escrevermosNPImagem( (nlins, ncols), val)
. Aquinlins
encols
são dois inteiros que definem a dimensão \(nlins \times ncols\) do array que representa a imagem eval
é o valor inicial de cada um de seus pixels. Caso oval
não seja especificado na chamada, a imagem deve ser criada tendo 0 como valor de cada pixel.
Importante: caso o tipo do val
seja um array do Numpy, a NPImagem criada deve usar esse array como seu conteúdo inicial. Veja nos exemplos que os valores de nlins
e ncols
não importam se val
é um array.
- A função
print()
deve exibir um objeto NPImagem simplesmente mostrando o conteúdo do atributo data. Esse item é muito importante para ajudar você a depurar sua classe. img.shape
. O valor do atributo shape de um objetoimg
da classe NPImagem deve ser a tupla(nlins, ncols)
que indica a dimensão deimg
.
classe NPImagem:
def __init__(self, shape=(0,0), val = 0):
''' Construtor da classe NPImagem
'''
if type(val) is np.ndarray:
self.data = val ## compartilha dados com val
else:
self.data = np.full( shape, val )
self.shape = self.data.shape
def __str__(self):
return str(self.data)
14.5. Exercícios¶
Complete a classe NPImagem
tal que:
Valor de um pixel: implemente o método
__getitem_()
para permitir que, ao escrevermosimg[lin, col]
, tenhamos o valor do pixel em [lin, col].Alterando o valor de um pixel: implemente o método
__setitem__()
para permitir que, ao escrevermosimg[lin, col] = val
, o valorval
seja armazenado no pixel [lin, col].- Método
crop()
: recebe 4 inteirossup
,esq
,inf
edir
que definem uma região retangular deimg
onde: esq
indica a primeira coluna,dir
a última coluna,sup
a primeira linha einf
a última linha do retângulo.
Você pode entender esses inteiros como as coordenadas do canto superior-esquerdo (sup,esq) e do canto inferior-direirto (inf,dir) do retângulo. Assim, se
img
é um objeto NPImagem, entãoimg.crop(sup, esq, inf, dir)
deve retornar uma nova NPImagem com a subimagem deimg
definida porsup
,esq
,inf
edir
. Caso esses quatro valores não sejam especificados na chamada, o método considerasup
eesq
como sendo zero, a origem da imagem, einf
edir
como as dimensões da imagem. Nesse caso, portanto, a chamada retorna um clone de img. O exemplo também mostra quando apenas o canto (sup, esq) é fornecido.
- Método
Você pode estender sua classe com outros métodos, atributos e usar outras funções caso desejar, desde que eles não entrem em conflito ou modifiquem o comportamento da classe NPImagem, como especificado nesse texto.
14.5.1. DICAS¶
- Não faça nada do zero. Utilize o que você já implementou nos EPs anteriores e apenas faça as modificações necessárias para criar a classe NPImagem.
- Para fazer recortes com crop(), você pode utilizar fatias de arrays. Além de ser mais eficiente, você economiza tempo escrevendo menos código.
14.5.2. exemplos¶
Estude os exemplos a seguir para compreender o comportamento esperado de um objeto do tipo NPImagem.
lista = list(range(20))
ar = np.array(lista).reshape(4,5)
img1 = NPImagem( (0, 0), ar) #
print(f"img1:\n{img1}")
print(f"Shape de img1: {img1.shape}\n")
img2 = NPImagem( (4, 3), 100)
print(f"img2:\n{img2}")
print(f"Shape de img2: {img2.shape}\n")
img2[1,2] = -10
print(f"img2[1,2]={img2[1,2]}")
print(f"img2:\n{img2}\n")
img3 = img2.crop() ## cria uma cópia
print(f"img3:\n{img3}\n")
img4 = img1.crop(0, 1, 3, 4)
print(f"img4:\n{img4}\n")
img5 = NPImagem( (3,2) )
print(f"img5:\n{img5}\n")
img6 = img1.crop(1,2)
print(f"img6:\n{img6}\n")
O resultado deve ser:
img1:
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]
[15 16 17 18 19]]
Shape de img1: (4, 5)
img2:
[[100 100 100]
[100 100 100]
[100 100 100]
[100 100 100]]
Shape de img2: (4, 3)
img2[1,2]=-10
img2:
[[100 100 100]
[100 100 -10]
[100 100 100]
[100 100 100]]
img3:
[[100 100 100]
[100 100 -10]
[100 100 100]
[100 100 100]]
img4:
[[ 1 2 3]
[ 6 7 8]
[11 12 13]]
img5:
[[0 0]
[0 0]
[0 0]]
img6:
[[ 7 8 9]
[12 13 14]
[17 18 19]]
14.6. Onde estamos e para onde vamos?¶
O uso de ndarrays para representar imagens digitais é o exemplo que escolhemos para realçar o uso de fatias de arrays. Acreditamos que o conceito de imagem seja simples de entender e divertido de manipular usando operações “globais”, ou seja, em toda a imagem. Vimos que, ao realizar as operações com arrays, ou fatias de arrays, podemos evitar o uso de laços encaixados, simplificando o código e permitindo que a gente resolva problemas pensando em imagens, ao invés de pensar nos pixels e nos laços para varrer as imagens.
Na próxima aula vamos continuar a estender a classe NPImagem para continuar a treinar o uso de arrays e a habilidade de abstração de dados.