Junho 2023
O comprimento de onda que nossos olhos podem detectar (o espectro de luz visível) é apenas uma fração do espectro de energia eletromagnético. As ondas mais curtas visíveis são as ondas no espectro do azul. Ondas com comprimento de onda mais curtos, como o ultra-violeta ainda são detectáveis, apenas não são visíveis ao olho humano. Na outra ponta do espectro visível temos as ondas vermelhas. Onda infravermelhas, como as usadas em controle-remoto são comunmente usadas, mas não vísiveis.
Se o espectro de luz vísivel for dividido, as cores predominantes são o vermelho, o verde e o azul (Red, Green, Blue - RGB). Essas cores são também chamadas de cores primárias. O modelo RGB é chamado de modelo de cor aditivo pois todas as outras cores são combinações das cores primárias. Por exemplo, o ciano é a mistura do azul puro com o verde puro.
Televisores e monitores de computador (atualmente praticamente indistinguiveis uns dos outros atualmente) criam cores usando as cores primárias da luz. Cada pixel (a menor unidade de reprodução de luz de um dispotivo) em um monitor começa como preto (ausência de luz). Quando as unidades de luz vermelha, verde e azul de um pixel são iluminadas simultaneamente, esse píxel se torna branco. Controlando a quantidade de cada um das unidades de luz, chamadas de canais de cor, nós podemos representar as cores do espectro visível em um monitor.
Atualmente, o valor da cor de cada pixel é armazenado usando os valores de cada canal do modelo RGB. Cada canal é representado por um byte (8 bits). Como um byte pode representar no máximo 256 valores, cada canal varia de 0 (ausência total da cor) a 255 (presença total da cor). Como temos 3 canais de cores, são necessários 3 bytes (24 bits) para cada pixel de uma imagem! Pode não parecer muito atualmente, mas antigamente processar essa quantidade de dados era um grande gargalo...
Além dos três canais de cor (RGB), o modelo RGB Alpha utilizado pelos computadores atuais adicionaram um novo canal para transparência (alpha). Caso o valor do canal seja 0 o pixel será completamente transparente, caso o canal tenha o valor seja 255 o canal será completamente opaco.
Usando essa representação em três canais mais o canal de transparência (alfa), 256 valores por canal, podemos representar:
Geralmente, imagens são descritas em forma de matrizes. Por exemplo, uma imagem 29x20 tem 29 linhas e cada linha tem 20 pixels, num total de 580 pixels! Em geral é isso que chamamos de resolução da imagem. Um monitor Full HD suporta uma resolução de 1920 x 1080 pixels. Um monitor 4K suporta uma resolução de até 3840 x 2129 pixels.
Uma imagem em Python é representada por uma matriz onde cada elemento da matriz (linha, coluna) corresponde a um pixel. Se a imagem estiver no modo RGBA, cada pixel contém 4 valores. Se estiver no modo RGB contém 3 valores.
Filtros de imagem são manipulações dos pixels de uma imagem de modo a transforma-la mas sem descaracterizá-la por completo. Existem filtros complexos, tais como os filtros no Stories do Instagram, que requerem não apenas a manipulação pixel a pixel da imagem mas também a manipulação de várias características de uma imagem como um todo. Neste projeto específico vamos trabalhar apenas com filtros que manipulam os pixels de uma imagem, pois são mais simples de entender e implementar:
Nas próximas seções vamos apresentar a teoria de cada um dos filtros. Para o filtro negativo será fornecido o código exemplo para que vocês possam continuar o desenvolvimento dos demais filtros.
O filtro negativo nada mais é do que o inverso de cada canal de cor, exceto o canal alpha de transparência. Para fazer o inverso do canal basta subtrair 255 do valor atual dos canais R, G , B. Por exemplo, se o valor atual de um pixel é [80, 123, 14, 255]
, seu negativo será [175, 132, 241, 255]A REPL modelo do projeto está disponível aqui.
Vocês podem implementar todos os filtros no mesmo arquivo Python. O projeto já vem com duas imagens padrão de teste, mas vocês podem utilizar outras imagens se assim desejarem.
Os códigos usados para acessar os pixels de cada imagem já estão corretamente inseridos no arquivo main.py. Explicarei brevemente o que cada parte do código objetiva:
from PIL import Image
image = Image.open('gato2.jpg')
w = image.size[0]
h = image.size[1]
for y in range(0, h):
for x in range(0, w):
R, G, B = im.getpixel((x, y))
nR = 255- R
nG = 255- G
nB = 255- B
image.putpixel((x,y) , (nR, nG, nB))
image.show()
Enquanto o filtro negativo operava em cada canal independentemente, o filtro de escala de cinza é um pouco mais sofisticado. A ideia central do filtro de escala de cinza é calcular a média dos valores de cada canal e atrubuir essa média ponderada a cada canal.
A média ponderada de cada canal é dada pela fórmula: media = R * 0.2126 + G * 0.7152 + B * 0.0722
Por exemplo, se o valor atual de um pixel é [240, 123, 230, 255]
, seu pixel em escala de cinza será:media = (240 * 0.2126) + (123 * 0.7152) + (230 * 0.0722) = 155.96
Como cada pixel só pode ter valores entre 0 e 255, sem resto fracionário, a fração .96 precisa ser descartada
O pixel resultante terá o valor de [155, 155, 155, 255]
O filtro de sépia da uma cara antiga, "amarelada" às imagens. Para implementar o filtro de sépia, assim como no filtro de escala de cinza, é necessário ter conhecimento dos 3 canais de cores simultaneamente para cálculo de uma média.
A média ponderada de cada canal é dada pela fórmula:
novoR = R * 0.393 + G * 0.769 + B * 0.189
novoG = R * 0.349 + G * 0.686 + B * 0.168
novoB = R * 0.272 + G * 0.534 + B * 0.131
Após o cálculo da nova média ponderada de cada canal, basta atribuir os novos valores dos canais à imagem.
Enquanto o filtro de escala de cinza transforma uma imagem em tons de cinza, o filtro preto e branco transforma cada pixel da imagem em preto ou branco de acordo com um determinado parâmetro denominado separador.
A forma mais fácil (não necessariamente a melhor...) de calcular o valor separador é assumir que o separador vale 255 e a partir dai ajustar o separador para valores próximos que produzam um resultado semelhante.
Você deve primeiramente somar o valor de cada um dos canais (R+G+B). Caso essa soma seja maior do que o valor do separador, atribuia a todos os canais o valor de 255 (tornando o pixel branco). Caso contrário, se a soma for menor do que o separador, atribua a todos os canais o valor de 0 (tornando o pixel preto)
Nesse filto vocês também vão precisar ter acesso e manipular os três canais de cores simultaneamente.
Para esse filtro, basta trocar os valores dos canais de cores da seguinte forma:
Esse filtro deve ser implementado por 3 funções diferentes. A ideia é intensificar cada canal de cor, de forma separada, em 10%.
Por exemplo, na função de intensificar o canal vermelho, basta multiplicar apenas o canal vermelho por 1.1.
Esse filtro deve ser implementado por 2 funções diferentes: uma para aumentar e outra para diminuir. A ideia é aumentar (ou diminuir) todos os canais de cor em 20%.
Este filtro é o mais complexo pois é um filtro que involve não apenas alterar o canal de cor de cada pixel, mas sim realizar uma média nos pixels adjacentes.
O filtro médio é usado para desfocar uma imagem para remover o ruído. Envolve determinar a média dos valores de pixel dentro de um kernel 3 x 3. A intensidade de pixel do elemento central é então substituída pela média. Isso elimina parte do ruído na imagem e suaviza as bordas da imagem.