Manuseio e Processamento Básico de Imagens
- Vinicius Goia
- 20 de out. de 2023
- 8 min de leitura

No artigo Geometria Primitiva e Transformações 2D com Python, entendemos como são realizadas as transformações, utilizando polígonos como base de exemplo. Agora iremos aplicar as transformações em imagens reais, explorando algumas bibliotecas disponíveis em Python. Além das transformações mais simples, verificaremos algumas técnicas envolvendo equalização de histogramas, filtros, remoção de ruídos e uma aplicação para contagem de objetos.
Transformações em Imagens
Para a maioria das transformações podemos utilizar uma biblioteca em Python chamada PIL. Com ela, conseguimos carregar uma imagem e realizar os procedimentos de redimensionamento, corte, rotação, dentre outros.
Nós códigos abaixo, iremos exemplificar alguns desses métodos.
#Importação de bibliotecas necessárias
from PIL import Image
from pylab import *

#Informações da Imagem
pil_im.show
<bound method Image.show of <PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=1024x1024 at 0x78D684652530>>
Observa-se que nossa imagem está no formato Jpeg, com 3 camadas de cores (RGB), no tamanho 1024x1024 pixels. A partir de agora, através da variável criada, podemos realizar algumas transformações.
Como, por exemplo, rotacionar nossa imagem:
#Rotacionar imagem
imshow(pil_im.rotate(45))

Ou recortar uma região:
#Definição dos pontos do retângulo de corte
box = (200,200,800,600)
#Recorte da imagem
region = pil_im.crop(box)
#Exibir imagem
imshow(region)

Podemos também inserir a imagem recortada novamente à imagem original, porém rotacionada 180°:
#Rotacionar e colar imagem
pil_im.paste(region.rotate(180),box)
#Exibir imagem
imshow(pil_im)

E com apenas um comando, podemos criar uma Thumbnail:

Outra possibilidade é a criação de pontos e linhas dentro da imagem. Para isso, temos que transformar nossa imagem em um array. Isso possibilita que trabalhemos nossas operações de forma matricial, ou seja, conseguimos entender nossa imagem como uma matriz e aproveitamos suas propriedades.
No exemplo abaixo, criaremos alguns pontos e linhas.
#Salvar imagem em uma variável
pil_im = Image.open('/content/Imagem.jpg')
#Redimensionar imagem
pil_im = pil_im.resize((600,600))
#Ler imagem como um array
im = array(pil_im)
#Imprimir imagem
imshow(im)
#Definição de pontos
x = [100,100,400,400]
y = [200,300,200,300]
#Plotar os pontos com marcação em estrela
plot(x,y,'r*')
#Conectar os pontos alterando o traçado
plot(x[:2],y[:2],'go-')
plot(x[1::2],y[1::2],'ys:')
#Adicionar titutlo e exibir a imagem
title('Imagem"')
axis('off')
show()

Transformações Graylevel
Muitas funções em Python trabalham diretamente com escala de cinza devido a facilidade em processar imagens com apenas um canal de cor. Diferentemente das imagens binárias, onde há apenas a presença do branco (255) ou preto (0), a escala de cinza tem toda a variação entre os limites.
Nos códigos abaixo, iremos exemplificar como transformar uma imagem em escala de cinza e verificar algumas variações:
#Carregando a imagem em uma variável
im_o = Image.open('/content/Imagem.jpg')
#Carregando imagem graylevel em uma variável
im = Image.open('/content/Imagem.jpg').convert('L')
#Transformando em array
im_o = array(im_o)
im = array(im)
#Exibindo propriedades imagem original
print (im_o.shape, im.dtype)
#Exibindo propriedades imagem grayscale
print (im.shape, im.dtype)
(1024, 1024, 3) uint8
(1024, 1024) uint8
#Variações
im2 = 255 - im #Inversão
im3 = (100.0/255) * im + 100 #Limitação de intervalo entre 100 e 200
im4 = 255.0 * (im/255.0) ** 2 #Transformação quadrática
#Configurações de plotagem
fig, [ax1,ax2,ax3,ax4,ax5] = plt.subplots(nrows=1, ncols=5, figsize=(20,30))
#Plots
ax1.imshow(im_o)
ax1.set_title('Imagem Original')
ax2.imshow(im,cmap='gray')
ax2.set_title('Imagem Grayscale')
ax3.imshow(im2,cmap='gray')
ax3.set_title('Imagem Invertida')
ax4.imshow(im3,cmap='gray')
ax4.set_title('Imagem com Limitação de Intensidade')
ax5.imshow(im4,cmap='gray')
ax5.set_title('Transformação Quadrática')
print('Imagem Original entre {} e {}'.format(int(im_o.min()), int(im_o.max())))
print('Imagem Grayscale entre {} e {}'.format(int(im.min()), int(im.max())))
print('Imagem Invertida entre {} e {}'.format(int(im2.min()), int(im2.max())))
print('Imagem com Limitação de Intensidade entre {} e {}'.format(int(im3.min()), int(im3.max())))
print('Imagem com Transformação Quadrática entre {} e {}'.format(int(im4.min()), int(im4.max())))

Equalização de Histogramas
Com os histogramas podemos observar de forma gráfica a variação da intensidade dos pixels em cada camada. A partir do código abaixo, é possível visualizar de forma separada as camadas da imagem, bem como seus histogramas.
#Salvar imagem em uma variável
im = Image.open('/content/Imagem.jpg')
#Separação das camadas em variáveis
R,G,B = im.split()
#Configurações de plotagem
fig, ax = plt.subplots(ncols=4, figsize=(15,15))
ax = ax.flatten()
#Plots
ax[0].imshow(im)
ax[0].set_title('Imagem Original')
ax[1].imshow(R,cmap='gray')
ax[1].set_title('Canal Red')
ax[2].imshow(G,cmap='gray')
ax[2].set_title('Canal Green')
ax[3].imshow(B,cmap='gray')
ax[3].set_title('Canal Blue')
show()

#Configurações de Plotagem
fig, ax = plt.subplots(ncols=4, figsize=(20,5))
ax = ax.flatten()
#Transformação das imagens em arrays
im = array(im)
R = array(R)
G = array (G)
B = array(B)
#Plots
ax[0].hist(im.flatten(), 128)
ax[0].set_title('Imagem Original')
ax[1].hist(R.flatten(), 128)
ax[1].set_title('Canal Red')
ax[2].hist(G.flatten(), 128)
ax[2].set_title('Canal Green')
ax[3].hist(B.flatten(), 128)
ax[3].set_title('Canal Blue')
show()

A equalização de histograma nos permite alcançar um equilíbrio maior entre as intensidades dos pixels, gerando imagens com tons mais normalizados. Vamos aplicar a equalização de histograma em cada camada da imagem, bem como em sua forma original. Para esta aplicação, criaremos uma função que utiliza a função de distribuição cumulativa, bem como interpolação linear para encontrar novos valores de pixels.
def histeq(im,nbr_bins=256):
"""Equalização de histograma em imagem graylevel."""
#Gerar histograma da imagem
imhist, bins = histogram(im.flatten(), nbr_bins, normed=True)
cdf = imhist.cumsum() #Função de distribuição cumulativa (cdf)
cdf = 255 * cdf / cdf[-1] #Normalização
#Utilizar Interpolação Linear da função de distribuição cumulativa para achar novos valores de pixel
im2 = interp(im.flatten(), bins[:-1], cdf)
return im2.reshape(im.shape), cdf
#Aplicação da esqualização de histograma
Req, Rcdf = histeq(R)
Geq, Gcdf = histeq(G)
Beq, Bcdf = histeq(B)
#Configurações de Plotagem
fig, ax = plt.subplots(ncols=3, nrows=3, figsize=(15,10))
ax = ax.flatten()
#Plots
ax[0].imshow(Req,cmap='gray')
ax[0].set_title('Canal Red')
ax[0].axis('off')
ax[1].imshow(Geq,cmap='gray')
ax[1].set_title('Canal Green')
ax[1].axis('off')
ax[2].imshow(Beq,cmap='gray')
ax[2].set_title('Canal Blue')
ax[2].axis('off')
ax[3].hist(Req.flatten(),128)
ax[3].set_title('Histograma Canal Red')
ax[3].axis('off')
ax[4].hist(Geq.flatten(),128)
ax[4].set_title('Histograma Canal Green')
ax[4].axis('off')
ax[5].hist(Beq.flatten(),128)
ax[5].set_title('Histograma Canal Blue')
ax[5].axis('off')
ax[6].plot(Rcdf)
ax[6].set_title('Curva CDF Canal Red')
ax[6].axis('off')
ax[7].plot(Gcdf)
ax[7].set_title('Curva CDF Canal Blue')
ax[7].axis('off')
ax[8].plot(Bcdf)
ax[8].set_title('Curva CDF Canal Green')
ax[8].axis('off')
show()

Como podemos observar, os histogramas foram equalizados de uma maneira mais uniforme. Para o canal vermelho, a equalização não fez muito sentido devido a ausência da cor na imagem, gerando uma saída com bastante brilho depois do processo.
Filtros
Os filtros são ferramentas que alteram as propriedades das imagens. Aqui iremos analisar as aplicações para dois dos principais filtros mais utilizados: o Gaussiano, associado a ação de desfocar uma imagem, e os Derivados, mais associados ao ato de enaltecer contornos.
O desfoque Gaussiano é um exemplo de convolução de imagem onde esta é processada por um kernel gaussiano para criar uma versão desfocada.
O kernel gaussiano 2D com desvio padrão é representado pela equação abaixo:
O SciPy possui um módulo para filtros chamado scipy.ndimage.filters o qual pode ser usado para processar convoluções usando separação rápida 1D. Nos códigos abaixo, iremos exemplificar o uso desses filtros.
#Importação de bibliotecas necessárias
from PIL import Image
from numpy import *
import matplotlib.pyplot as plt
from scipy.ndimage import filters
#Carregamento de imagem em graylevel em uma variável e aplicação do Filtro Gaussiano
im = array(Image.open('/content/Imagem.jpg').convert('L'))
im2 = filters.gaussian_filter(im,5)
#Configurações de plotagem
fig, [ax1, ax2] = plt.subplots(ncols=2,figsize=(15,15))
#Plots
ax1.imshow(im,cmap='gray')
ax2.imshow(im2,cmap='gray')
plt.show()

O parâmetro 5 utilizado em gaussian_filter() é o desvio padrão. Quanto mais elevado, maior será o desfoque.
Para o uso desse filtro em uma imagem colorida, devemos utilizar uma iteração para que o filtro seja aplicado nas 3 camadas de cores, como no exemplo abaixo:
#Carregamento de imagem colorida e criação de matriz de zeros
im = array(Image.open('/content/Imagem.jpg'))
im2 = zeros(im.shape)
#Iteração para aplicação de filtro Gaussiano em cada camada de cor
for i in range(3):
im2[:,:,i] = filters.gaussian_filter(im[:,:,i],5)
im2 = uint8(im2)
#Configurações de plotagem
fig, [ax1, ax2] = plt.subplots(ncols=2,figsize=(15,15))
#Plots
ax1.imshow(im)
ax2.imshow(im2)
plt.show()

Já os filtros derivados trabalham com a variação de intensidade dos pixels, que são as derivadas de x e y de uma imagem em escala de cinza.
Nesse processo, temos os seguintes componentes:
Que é o vetor.
Que descreve o quão forte é a mudança de intensidade de cada pixel em uma imagem, e:
Que indica a direção da maior mudança de intensidade para cada pixel.
Para processar imagens derivadas podemos utilizar aproximações discretas, que podem ser facilmente implementadas como convoluções:
Em nosso exemplo, iremos utilizar o Filtro Sobel, onde Dx e Dy são:
#Carregamento de imagem em graylevel
im = array(Image.open('/content/Imagem.jpg').convert('L'))
#Filtros de derivadas Sobel
imx = zeros(im.shape)
filters.sobel(im,1,imx)
imy = zeros(im.shape)
filters.sobel(im,0,imy)
magnitude = sqrt(imx**2+imy**2)
#Configurações de plotagem
fig, [ax1, ax2,ax3, ax4] = plt.subplots(ncols=4,figsize=(15,15))
#Plots
ax1.imshow(im, cmap='gray')
ax1.set_title('Original')
ax1.set_axis_off()
ax2.imshow(imx,cmap='gray')
ax2.set_title('X Derivative')
ax2.set_axis_off()
ax3.imshow(imy,cmap='gray')
ax3.set_title('Y Derivative')
ax3.set_axis_off()
ax4.imshow(magnitude,cmap='gray')
ax4.set_title('Magnitude')
ax4.set_axis_off()
plt.show()

Podemos utilizar as imagens derivadas juntamente com o filtro gaussiano, conforme exemplo abaixo:
#Desvio padrão para o filtro Gaussiano
sigma = 10
#Filtros de derivadas Gaussianas
imx = zeros(im.shape)
filters.gaussian_filter(im,(sigma,sigma),(0,1),imx)
imy = zeros(im.shape)
filters.gaussian_filter(im,(sigma,sigma),(1,0),imy)
#Configurações de plotagem
magnitude = sqrt(imx**2+imy**2)
#Plots
fig, [ax1, ax2,ax3, ax4] = plt.subplots(ncols=4,figsize=(20,30))
ax1.imshow(im, cmap='gray')
ax1.set_title('Original')
ax1.set_axis_off()
ax2.imshow(imx,cmap='gray')
ax2.set_title('X Gaussian Derivative')
ax2.set_axis_off()
ax3.imshow(imy,cmap='gray')
ax3.set_title('Y Gaussian Derivative')
ax3.set_axis_off()
ax4.imshow(magnitude,cmap='gray')
ax4.set_title('Gaussian Magnitude')
ax4.set_axis_off()
plt.show()

Redução de Ruídos
As técnicas de redução de ruído são consideradas processos de baixo nível em visão computacional. Estão no mesmo grupo das técnicas de aprimoramento de contraste e contornos, onde as entradas e saídas são apenas imagens. Iremos exemplificar uma técnica baseada no modelo Rudin-Osher-Fatemi (ROF) de redução de ruído. Para isso, criaremos uma função para ajuste de parâmetros e aplicaremos em uma imagem com ruído.
#Importação de bibliotecas necessárias
from numpy import *
#Definição da função ROF
def denoise(im, U_init, tolerance=0.1, tau=0.08, tv_weight=100):
"""Uma implementação do modelo de remoção de ruído Rudin-Osher-Fatemi (ROF) usando o procedimento numérico apresentado na equação (11) A. Chambole (2005).
Entrada: imagem de entrada com ruído (tons de cinza), estimativa inicial para U, peso do termo de regularização de TV, comprimento de passo, tolerância para critério de parada.
Saída: imagem sem ruído e sem textura, textura residual."""
m,n = im.shape #Tamanho da imagem com ruído
#Inicialização
U = U_init
Px = im #x-componente para campo duplo
Py = im #y-componente para campo duplo
error = 1
while (error > tolerance):
Uold = U
#Gradiente da primeira variável
GradUx = roll(U,-1,axis=1)-U #x-componente de U's gradiente
GradUy = roll(U,-1,axis=0)-U #y-componente de U's gradiente
#Atualização da variável dupla
PxNew = Px + (tau/tv_weight)*GradUx
PyNew = Py + (tau/tv_weight)*GradUy
NormNew = maximum(1,sqrt(PxNew**2+PyNew**2))
Px = PxNew/NormNew #Atualização da variável dupla x
Py = PyNew/NormNew #Atualização da variável dupla y
#Atualização da primeira variável
RxPx = roll(Px,1,axis=1) #Translação para direita do componente x
RyPy = roll(Py,1,axis=0) #Translação para direita do componente y
DivP = (Px-RxPx)+(Py-RyPy) #Divergência do campo duplo
U = im + tv_weight*DivP #Atualização da primeira variável
#Atualização de erro
error = linalg.norm(U-Uold)/sqrt(n*m);
return U,im-U #Imagem sem ruido e textura residual
#Importação de bibliotecas necessárias
from numpy import *
from numpy import random
from scipy.ndimage import filters
#Carregamento da imagem em graylevel
im = array(Image.open('/content/ruido.jpg').convert('L'))
#Aplicação de redução de ruído e filtro gaussiano
U,T = denoise(im,im)
G = filters.gaussian_filter(im,10)
#Configuração de plotatem
fig, ax = plt.subplots(ncols=3, figsize=(15,15))
ax = ax.flatten()
#Plots
ax[0].imshow(im,cmap='gray')
ax[0].set_axis_off()
ax[0].set_title('Original Noisy Image')
ax[1].imshow(G,cmap='gray')
ax[1].set_axis_off()
ax[1].set_title('Gaussian Blurring')
ax[2].imshow(U,cmap='gray')
ax[2].set_axis_off()
ax[2].set_title('ROF De-noising')

Contador de Objetos
Para contarmos objetos em uma imagem, necessitamos da utilização de técnicas de segmentação. Apesar de o exemplo utilizado aqui ser bem simplista, esse tipo de processamento é considerado de nível intermediário, pois a entrada é uma imagem e a saída são atributos, no caso, a quantidade de objetos. Para esta aplicação, utilizaremos as bibliotecas do OpenCV para criação de uma imagem binária e do SciPy para contagem.
#Importação de bibliotecas necessárias
import cv2
from scipy.ndimage import filters
#Carregamento de imagem, transformação em escala de cores HSV e aplicação de desfoque
im = array(Image.open('/content/macieira.jpg'))
hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
blur = cv2.medianBlur(hsv ,7)
#Definição do intervalo de cores
lower = np.array([100,100,50])
upper = np.array([353,353,353])
#Criação da máscara
mask = cv2.inRange(blur, lower, upper)
#Aplicação da máscara à imagem
res = cv2.bitwise_and(blur,blur, mask= mask)
#Configuração de plotagem
fig, axes = plt.subplots(ncols=3,figsize=(15, 15))
ax = axes.flatten()
#Plots
ax[0].imshow(im, cmap='gray')
ax[0].set_title('Original')
ax[0].set_axis_off()
ax[1].imshow(mask,cmap='gray')
ax[1].set_title('Binary')
ax[1].set_axis_off()
#Transformação de escala de cor em BGR
res = cv2.cvtColor(res, cv2.COLOR_HSV2BGR)
#Plots
ax[2].imshow(res)
ax[2].set_title('Result')
ax[2].set_axis_off()
plt.show()

No processo acima, carregamos a imagem de uma macieira, transformamos em escala de cores HSV e aplicamos um desfoque. Definimos o intervalo de cores para identificação dos tons de vermelho e criamos uma máscara. Nos códigos abaixo, essa máscara será submetida à um método para contagem.
#Importação de biblioteca necessária
from scipy.ndimage import label, morphology
#Aplicação dos métodos e apresentação do resultado
labels, nbr_objects = label(mask)
print('Number of objects:', nbr_objects)
Number of objects: 102
Se contarmos de forma visual quantas maçãs aparecem na imagem, verificaremos que o nosso código está retornando um valor que não condiz com a realidade. Isso acontece devido a imagem possuir muitas folhas que cobrem algumas maçãs e essa separação faz com que a função considere 2 objetos ou mais ao invés de 1. Para diminuir esse erro, iremos utilizar uma outra função que expande a área contrária aos objetos separados, fazendo com que eles encolham. Desta maneira, as classificações que possuem um tamanho muito pequeno irão desaparecer.
#Morphology - Expandindo para separar os objetos de uma melhor maneira
im_open = morphology.binary_opening(mask, ones((9,5)),iterations=2)
#Aplicação dos métodos e apresentação do resultado
labels_open, nbr_objects_open = label(im_open)
print('Number of objects:', nbr_objects_open)
#Configuração de plotagem
fig, axes = plt.subplots(ncols=2,figsize=(15, 15))
ax = axes.flatten()
#Plots
ax[0].imshow(mask, cmap='gray')
ax[0].set_title('Binary')
ax[0].set_axis_off()
ax[1].imshow(im_open,cmap='gray')
ax[1].set_title('Binary Opened')
ax[1].set_axis_off()
plt.show()
Number of objects: 37

É claro que, para uma contagem mais precisa, seria interessante a implementação desse código. Porém, para uma estimativa, essa linha de raciocínio já é suficiente.
Conclusão
Neste artigo exemplificamos alguns processamentos básicos de imagem, finalizando com uma aplicação que pode ser considerada intermediária na área de visão computacional. Apesar de simples, todos os métodos aqui apresentados constituem a base das áreas de Processamento de Imagem e Visão Computacional. A maioria dos algoritmos desenvolvidos nesses segmentos utilizam esses conceitos para criação das mais diversas aplicações. Obviamente, o objetivo deste artigo é apresentar algumas ideias e a implementação é mais que bem-vinda. Todos os códigos aqui apresentados foram baseados no livro Programming Computer Vision with Python, de Jan Erik Solem.
Comments