Prazo de entrega recomendado:
Nesta tarefa iremos trabalhar com algumas operações presentes em processamento de imagens. Você deve manipular matrizes para aplicar filtros em imagens e deverá manipular arquivos para armazená-las.
O processamento de imagens está presente em várias áreas da computação e é responsável por tarefas como classificação, extração de características, reconhecimento de padrões, entre outros. É caracterizado como um algoritmo que recebe uma imagem como entrada, produzindo outra imagem como saída.
Nesta tarefa, trabalharemos com dois tipos de formatos de imagem, PBM e PPM. Ambos trazem uma representação simplificada (sem compressão de dados), o que facilitará nossa utilização. O primeiro formato trata de imagens que podem ser representadas apenas por branco e preto, enquanto o segundo é um formato para imagens coloridas. Mais informações sobre os formatos podem ser vistas aqui.
1. Imagens monocromáticas
Imagens preto e branco podem ser facilmente descritas por uma matriz de bits, onde 0 e 1 representam, respectivamente, pixels branco e preto. Este é o objetivo do formato PBM: a primeira linha indica o tipo do arquivo, P1; a segunda linha indica as dimensões da imagem, largura e altura; cada linha seguinte representa uma linha da imagem, conforme descrito acima.
Em seguida, temos um arquivo PBM e sua imagem correspondente (ampliada, para facilitar a visualização).
P1
7 11
0 0 0 0 0 0 0
0 0 0 0 0 1 0
0 0 0 0 0 1 0
0 0 0 0 0 1 0
0 0 0 0 0 1 0
0 0 0 0 0 1 0
0 0 0 0 0 1 0
0 1 0 0 0 1 0
0 0 1 1 1 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
Detectando bordas
Em processamento de imagens, uma tarefa recorrente é detectar as fronteiras de objetos em uma imagem. A borda de um objeto é formada pelos pixels do objeto que têm ao menos um vizinho que não está no objeto, ou seja, um pixel 0. A vizinhança considera os 8 pixels ao redor do pixel central.
A seguir, podemos ver um exemplo de imagem binária e a sua imagem de bordas.
Você deve implementar um programa bordas.py
que, dada uma imagem
descrita no formato PBM, produza uma imagem de suas bordas, descrita
no mesmo formato. Você deverá utilizar manipulação de arquivos em
Python para ler/escrever essas imagens. O seu programa receberá dois
parâmetros de linha comando representando, respectivamente, o nome da
imagem original e a imagem com as bordas detectadas (que será criada
pelo seu programa).
user@host:../tarefa07$ ls
aviao.pbm bordas.py testar.py testes/
user@host:../tarefa07$ python3 bordas.py aviao.pbm aviao_bordas.pbm
user@host:../tarefa07$ ls
aviao.pbm bordas.py aviao_bordas.pbm testar.py testes/
2. Imagens coloridas
Podemos representar imagens coloridas (RGB) com o formato PPM, que se assemelha ao formato anterior ao descrever os pixels da imagem, linha a linha. Neste caso, porém, damos três valores para cada pixel, um para cada canal (red, green, blue), que formam a cor do pixel.
Veja abaixo o formato do arquivo PPM (note que o 255 na terceira linha indica o valor máximo de cada cor, e sempre estará presente).
P3
3 2
255
255 0 0 0 255 0 0 0 255
255 255 0 255 255 255 0 0 0
Aplicando filtros
Filtros de convolução são usados em imagens para aplicar diversos efeitos, como blur, sharpening, detecção de bordas, entre outros. Estes filtros são descritos através de um kernel, que é essencialmente uma pequena matriz, geralmente 3x3. A imagem com o efeito é o resultado da convolução da imagem original com o kernel.
A convolução é um processo operado em cada pixel da imagem original: somam-se os valores dos pixels em sua volta, multiplicados pelo valor daquela posição no kernel.
Suponha que o kernel seja a matriz à esquerda, enquanto a matriz à direita seja uma parte da imagem, representando os valores dos pixels. Neste momento, estamos calculando o pixel da posição cujo valor na imagem original é 25. O valor deste pixel na imagem com filtro, após a convolução, será
$$ a \times 10 + b \times 10 + c \times 10 + d \times 10 + e \times 25 + f \times 10 + g \times 10 + h \times 10 + i \times 10. $$
Quando realizamos esse procedimento em todos os pixels da imagem, temos a imagem filtrada.
Temos que tomar alguns cuidados ao escrever um algoritmo para aplicar um filtro de convolução. Se ao aplicarmos o kernel em um pixel, algum vizinho desse pixel estiver fora da imagem (ou seja, se o pixel central estiver na borda), então a posição desse pixel na imagem filtrada receberá valor 0. Além disso, o valor calculado pode sair dos níveis mínimo e máximo (0 e 255). Portanto, é a sua responsabilidade impor estes limites na imagem com filtro
Como nós aplicaremos a convolução em uma imagem PPM, que tem três canais de cor, iremos realizar a operação de convolução separadamente em cada cor, somando apenas valores do mesmo canal. Note que a soma opera somente sobre os valores dos pixels na imagem original, nunca sobre valores recém calculados.
Escreva um programa filtros.py
que receba três parâmetros de linha
comando que representam, respectivamente, o nome do filtro utilizado,
o nome da imagem original e o nome da imagem com filtro. Assim como no
primeiro exercício, você deve ler e escrever arquivos para manipular
as imagens.
Seu programa deve implementar um filtro bordas
, cujo kernel $K$ está
descrito abaixo. Seu programa será avaliado somente com esse filtro
nas imagens fornecidas.
$$ K = \begin{pmatrix} -1 & -1 & -1 \\ -1 & 8 & -1 \\ -1 & -1 & -1 \end{pmatrix} $$
Uma chamada usual de seu programa terá o formato abaixo.
user@host$ python3 filtros.py bordas bike.ppm bike_bordas.ppm
Esse comando recebe a imagem original e cria uma nova imagem modificada com o filtro de detecção de bordas aplicado. Veja o exemplo abaixo.
Mais filtros (opcional)
Existem muitos outros filtros baseados em kernel para processamento de imagens, veja aqui e aqui. O seu programa é genérico para aplicar qualquer kernel $n \times n$?
filtro_sharpen = [[0, -1, 0], [-1, 5, -1], [0, -1, 0]]
filtro_left_sobel = [[1, 0, -1], [2, 0, -2], [1, 0, -1]]
filtro_emboss = [[-2, -1, 0], [-1, 1, 1], [0, 1, 2]]
filtro_blur = [[0.04, 0.04, 0.04, 0.04, 0.04], [0.04, 0.04, 0.04, 0.04, 0.04], [0.04, 0.04, 0.04, 0.04, 0.04], [0.04, 0.04, 0.04, 0.04, 0.04], [0.04, 0.04, 0.04, 0.04, 0.04] ]
filtro_unsharp = [[-0.00390625, -0.015625, -0.0234375, -0.015625, -0.00390625], [-0.015625, -0.0625, -0.09375, -0.0625, -0.015625], [-0.0234375, -0.09375, 1.859375, -0.09375, -0.0234375], [-0.015625, -0.0625, -0.09375, -0.0625, -0.015625], [-0.00390625, -0.015625, -0.0234375, -0.015625, -0.00390625]]
Dicas
-
Pode ser útil olhar para a imagem em um visualizador. Em distribuições GNU/Linux, a maioria dos visualizadores podem abrir arquivos PBM ou PPM facilmente. Em outros sistemas operacionais, você talvez precise instalar um visualizador ou editor de imagens, ou pode usar uma ferramenta de conversão online, como essa. Se seu visualizador não funcionar, você tentar fazer o download desse.
-
Em um terminal GNU/Linux, você pode converter suas próprias imagens para PPM e testar diferentes filtros usando um comando
convert
do pacote ImageMagick. Esse pacote já deve estar instalado. Note que o arquivo criado por este programa não seguirá necessariamente a formatação que apresentamos aqui (três canais por linha).user@host$ convert imagem.jpg -compress none imagem.ppm
-
A função
print
possui dois argumentos que controlam os separadores entre as variáveis e o caractere final. O código abaixo imprime10,20!
, sem um caractere\n
no final da linha.a = 10 b = 20 print(a, b, sep=',', end='!')
Testando seu programa
-
Como seu programa deverá ter várias funções — e há várias tarefas distintas a serem feitas, é importante testar cada função separadamente, à medida em que se implementa cada uma. Para isso, crie funções de teste, cada uma com o único objetivo de chamar uma função de seu programa e verificar se ela devolve o valor esperado. Por exemplo, suponha que você tem um função chamada
carregar_imagem
para ler uma imagem e que você represente uma imagem com uma matriz de inteiros, 0 ou 1. Podemos adicionar a seguinte função de teste.def testar_leitura_pbm(): largura, altura, imagem = carregar_imagem("testes/jota.pbm") assert largura == 7 assert altura == 11 matriz_esperada = [ [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 1, 0], [0, 1, 0, 0, 0, 1, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0] ] assert imagem == matriz_esperada testar_leitura_pbm()
O que esse programa faz é verificar se a imagem que a função implementada devolve é de fato a matriz de bits que fornecemos manualmente. Quando você executar essa função de teste, se você vir uma mensagem de erro, então sua função tem um problema; mas se nada aconteceu, então o teste passou.
-
Experimente criar outras funções de teste. Ao invés de adicionar uma função no seu programa principal, alguns programadores preferem criar arquivos separados de teste que importam a função sendo testada. O problema é que que não queremos executar a função
main
do programa quando o importamos como um módulo. Para resolver esse problema, podemos fazer o seguinte. Substitua a chamada à funçãomain
dobordas.py
pelo seguinte:if __name__ == "__main__": main()
Agora podemos criar um novo arquivo
testar_bordas.py
parecido com o seguintefrom bordas import destacar_bordas def testar_bordas(): # crie um exemplo de imagem bem pequena para testar largura = ... altura = ... imagem = [ ... ] # cria a matriz de bordas que você espera para essa imagem bordas_esperadas = [ ... ] # aqui chamamos a função sendo testada bordas_calculadas = destacar_bordas(largura, altura, imagem) # isso irá gerar um erro quando a função não estiver correta assert bordas_esperadas == bordas_calculadas # se o programa não falhou até chegar aqui, # então talvez sua função esteja correta testar_bordas()
Assim a função
main
só será chamada quando executarmos o programabordas.py
, não quando importamos o módulo a partir detestar_bordas.py
.
Correção
Depois de submetida e terminada a tarefa, você deverá apresentá-la a um monitor
PED. Para isso, você deve procurar atendimento em algum horário com monitor PED
e digitar apresentar 7 no canal fila-apresentar
.
Temos cinco casos de testes, dois para o primeiro problema e três para o segundo, onde o último é opcional (não altera sua nota). Ambos os problemas possuem imagens pequenas para ajudar no processo de resolução da tarefa, compare sua solução com essas imagens.
Para obter o conceito C, você precisa resolver o primeiro problema com todas as
imagens PBM; o conceito B é obtido resolvendo o segundo problema com as
imagens pequeno.ppm
e colored_star.ppm
, e o conceito A é obtido considerando
todas as imagens PPM no segundo problema.
A pasta da tarefa no seu repositório vem inicialmente com um arquivo
nao_corrigir.txt
que serve para indicar que você ainda está trabalhando nesta
tarefa. Você pode realizar quantos pushes quanto forem necessários. O monitor só
irá corrigir sua tarefa quando esse arquivo for removido do repositório.