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 `__.