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**.
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 em ``ndarray``;
* usar objetos do tipo ``NPImagem`` em seus programas.
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 :math:`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.
.. raw:: html
.. exemplo
.. code:: python
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):
for col in range(e, d):
ar[lin, col] = v
print(ar)
Como fatias são vistas do array, podemos usar fatias de arrays para modificar regiões como:
.. code:: python
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:
.. code:: python
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.
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.
.. figure:: https://upload.wikimedia.org/wikipedia/commons/3/33/Neighborhood_watch_bw.png
:width: 200px
:alt: Imagem "Neighborhood_watch_bw"
:align: center
Exemplo de imagem binária. Fonte: `Binary image (Wikipedia) `__.
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.
.. figure:: https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png
:width: 200px
:alt: Exemplo de imagem em níveis de cinza
:align: center
Exemplo de imagem em níveis de cinza. Fonte: `Grayscale image (Wikipedia) `__.
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 `__.
.. figure:: https://upload.wikimedia.org/wikipedia/commons/thumb/c/ce/Anodorhynchus_hyacinthinus_-Disney_-Florida-8.jpg/800px-Anodorhynchus_hyacinthinus_-Disney_-Florida-8.jpg
:width: 200px
:alt: Exemplo de imagem colorida
:align: center
Exemplo de imagem colorida. Fonte: `Arara (Wikipedia) `__.
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 escrevermos ``NPImagem( (nlins, ncols), val)``. Aqui ``nlins`` e ``ncols`` são dois inteiros que definem a dimensão :math:`nlins \times ncols` do array que representa a imagem e ``val`` é o valor inicial de cada um de seus pixels. Caso o ``val`` 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 objeto ``img`` da classe NPImagem deve ser a tupla ``(nlins, ncols)`` que indica a dimensão de ``img``.
.. code:: python
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)
Exercícios
----------
Complete a classe ``NPImagem`` tal que:
#. Valor de um pixel: implemente o método ``__getitem_()`` para permitir que, ao escrevermos ``img[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 escrevermos ``img[lin, col] = val``, o valor ``val`` seja armazenado no pixel [lin, col].
#. Método ``crop()``: recebe 4 inteiros ``sup``, ``esq``, ``inf`` e ``dir`` que definem uma região retangular de ``img`` onde:
* ``esq`` indica a primeira coluna,
* ``dir`` a última coluna,
* ``sup`` a primeira linha e
* ``inf`` 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ão ``img.crop(sup, esq, inf, dir)`` deve retornar uma nova NPImagem com a subimagem de ``img`` definida por ``sup``, ``esq``, ``inf`` e ``dir``. Caso esses quatro valores não sejam especificados na chamada, o método considera ``sup`` e ``esq`` como sendo zero, a origem da imagem, e ``inf`` e ``dir`` 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.
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.
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.
exemplos
........
Estude os exemplos a seguir para compreender o comportamento esperado de um objeto do tipo NPImagem.
.. code:: python
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:
.. code:: python
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]]
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.
Para saber mais
---------------
* `NumPy: the absolute basics for beginners `__.
* `NumPy quickstart `__.
* `NumPy fundamentals `__.