15. Operações com imagens

Nessa aula vamos complementar a classe NPImagem, que começamos a construir na aula anterior, para fortalecer sua habilidade de abstração de dados e sua experiência com arrays.

15.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;
  • manipular imagens do tipo NPImagem, sem pensar nelas como arrays.

15.2. Introdução

Vamos primeiro pensar em algumas operações simples para manipular imagens como por exemplo:

  • recortar um pedaço (região retangular) de uma imagem;
  • pintar uma região com uma “cor” (nível de cinza); e
  • sobrepor uma imagem (uma região) sobre outra imagem.

Por exemplo, considere as duas imagens abaixo, que vamos chamar de Elvis e Einstein (obtidas da Wikipedia):

_images/elvis.png _images/einstein.png

Com essas duas imagens podemos compor outra imagem realizando as seguintes operações:

  • pintar de cinza uma área ao redor da cabeça de Elvis;
  • recortar uma região contendo a cabeça de Einstein;
  • colar a cabeça de Einstein sobre a cabeça de Elvis.
_images/composicao.png

Você mesmo pode compor essa imagem, talvez apenas tomando um certo cuidado com a escala, que não vamos tratar nessa aula. Para isso, você pode complementar a classe NPImagem da aula anterior com os métodos pinte_retangulo() e paste() como definidos a seguir:

def pinte_retangulo(self, sup, esq, inf, dir, v=0):
    ''' (NPImagem, int, int, int, int, int) -> None
    Recebe 4 inteiros que definem o canto superior-esquerdo (sup, esq) e
    o canto inferior-direito (inf,dir) de uma região retangular com
    relação a posição (0,0) de self, ou seja, os cantos são "deslocamentos"
    em pixeis com relação à origem.
    Esse método pinta, com o valor v, os pixeis de self que tenham sobreposição com o retângulo (sup,esq)x(inf,dir).
    '''


def paste(self, other, sup, esq):
    '''(NPImagem, NPImagem, int, int) -> None
    Recebe um objeto NPImagem other e um par de inteiros (sup, esq)
    que indica um deslocamento em relação à origem de self (posição (0,0))
    onde a NPImagem other deve ser sobreposta sobre self. Observe que
    esse deslocamento pode ser negativo. Caso não existir sobreposição,
    a imagem self fica inalterada.
    '''

Observe que o método paste deve permitir a sobreposição de uma imagem sobre outra. Imaginando a imagem self como referência e os índices de cada pixel como “coordenadas”, podemos considerar esse sistema de coordenadas para posicionar o canto superior-esquerdo da imagem other. Assumindo que essas coordenadas podem conter posições fora da imagem self (tanto posições negativas quanto maiores que as dimensões de self), o método paste() precisa calcular a área de sobreposição entre as imagens e trocar os valores dessa região apenas, como ilustra a figura abaixo, onde self é o retângulo azul e other é o retângulo vermelho. Observer que as dimensões de cada imagem podem ser diferentes.


Sobreposição.

Para tornar a atividade mais interessante, podemos carregar e exibir imagens podemos usar o módulo Matplotlib do Python. Esse módulo também é instalado pelo Anaconda, junto com o NumPy. Esse é um outro módulo bastante utilizado em computação científica para a construção e exibição de gráficos em geral.

Para carregar e exibir imagens usando Matplotlib use o seguinte trecho de código usando o Spyder em seu computador:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

from npimagem import NPImagem

def main():
    fname = 'elvis.png'
    image = mpimg.imread(fname)
    gray = np.array(image)[:,:,2] ## pega canal G da imagem RGB

    elvis = NPImagem( (), gray ) # transforma o array em uma NPImagem
    print("elvis.shape: ", elvis.shape)
    plt.gray()
    plt.imshow(elvis.data)
    plt.show()

    fname = 'einstein.png'
    image = mpimg.imread(fname)
    gray = np.array(image)[:,:,2] ## pega canal G da imagem RGB

    einstein = NPImagem( (), gray)
    print("einstein.shape", einstein.shape)

    plt.imshow(einstein.data)
    plt.show()

main()

Esse trecho ilustra também como podemos converter imagens do módulo matplotlib.image em imagens do tipo NPImagem. Para executar esse trecho, baixe as imagens na mesma pasta do seu computador contendo esse trecho de código e nomeie as imagens elvis.png e einstein.png.

Ao executar esse exemplo, você deve “fechar” a janela que exibe uma imagem para abrir a próxima janela. Modifique (estenda) esse programa para produzir as visualizações que você desejar.

15.3. Mais operações com imagens

Uma outra operação interessante e divertida é a mistura de duas imagens (blend). Duas imagens, \(I0\) e \(I1\) podem ser misturadas por meio da seguinte fórmula para gerar a imagem \(I2\).

\[I2 = \alpha \; I0 + (1 - \alpha) \; I1\]

Para entender essa fórmula, considere um valor de \(\alpha\) no intervalo [0.0, 1.0]. Para \(\alpha = 0.3\), por exemplo, a imagem \(I2\) resultante seria composta por 30% de \(I0\) e \(1 - 0.3 = 0.7\) (ou 70%) de \(I1\).

15.4. Exercícios

Estenda a classe NPImagem com os métodos definidos a seguir que permitam misturar imagens.

def __add__(self, other):
    ''' (NPImagem, NPImagem ou int ou float) -> NPImagem
    Quando recebe dois objetos NPImagem, retorna a soma, elemento-a-elemento,
    dos pixels de self e other.
    Quando other for int ou float, todos os elementos de self são adicionados de other.
    '''

def __mul__(self, other):
    ''' (NPImagem, NPImagem ou int ou float) -> NPImagem
    Quando recebe dois objetos NPImagem, retorna a multiplicação, elemento-a-elemento,
    dos pixels de self e other.
    Quando other for int ou float, todos os elementos de self são multiplicados por other.
    '''

Escreva também os métodos __radd__() e __rmul__().

Procure usar as propriedades de arrays do NumPy, evitando o uso de laços. Essas operações, realizadas diretamente com arrays, são conhecidas como vetoriais, ao invés de por-elemento.

Por fim, escreva também o seguinte método blend() na classe NPImagem:

def blend(self, other, alfa=0.5):
    ''' (NPImagem, NPImagem, float) -> NPImagem
    Recebe duas NPImagens e retorna uma nova NPImagem que mistura essas
    imagens com peso alfa e (1-alfa) tal que o resultado seja:
    self * alfa + other * (1-alfa)
    '''

O trecho de código a seguir ilustra o comportamento esperado desses métodos.

img1 = NPImagem( (3,5), 10.0 )
img2 = NPImagem( (3,5), 20.0 )
img3 = img1.blend(img2, 0.10)
print(img3)

deve resultar na saída (do print):

[[19. 19. 19. 19. 19.]
 [19. 19. 19. 19. 19.]
 [19. 19. 19. 19. 19.]]

15.5. Onde estamos e para onde vamos?

Nessa primeira parte da disciplina focamos no desenvolvimento da capacidade de abstrair diversos tipos de dados e utilizar essas abstrações em nossos programas. Vimos que a criação desses tipos é simplificada pela programação orientada a objetos. Os tipos podem ser armazenados em módulos, como os módulos NumPy e Matplotlib, que podem ser reutilizados por outros programas. Dessa forma, podemos construir tipos cada vez mais complexos, como imagens, tornando nossos programas cada vez mais poderosos.

Nas próximas aulas vamos começar a introduzir outro conceito fundamental do pensamento computacional, conhecido como recursão.

15.6. Para saber mais

O Python é conhecido por vir com suas “baterias incluídas”, ou seja, há módulos disponíveis para simplificar a programação em muitas áreas diferentes. Para saber ainda mais dos módulos disponíveis para a área científica, além de NumPy e Matplotlib, ainda podemos citar SciPy, Pandas, Statsmodel, Keras e TensorFlow. Basta fazer uma busca na Internet para conhecer mais sobre esses e outros módulos do Python.