Capítulo 2: Processamento de Imagens#

cover

Técnicas para o Pré-processamento de Imagens#

O pré-processamento visa preparar a imagem para análise, removendo ruídos e padronizando características. Segundo Gonzalez e Woods (2018), essas técnicas são cruciais para a eficiência de algoritmos de visão computacional e aprendizado de máquina.

Por que Pré-processar?#

  • Qualidade: Melhora o contraste e remove ruídos indesejados.

  • Padronização: Mantém a consistência entre diferentes conjuntos de dados.

  • Performance: Facilita a convergência de modelos de IA e reduz o custo computacional.

Principais Técnicas#

Técnica

Objetivo

Normalização

Ajustar intensidades (ex: [0, 1]) para estabilidade numérica.

Filtros Espaciais

Suavizar (Blur) ou realçar bordas (Sharpen).

Equalização

Maximizar o contraste global ou local da imagem.

Redimensionamento

Padronizar as dimensões para entrada em Redes Neurais (CNNs).

Normalização de Intensidade#

Ajustar os valores dos pixels é um passo fundamental antes do treinamento de modelos de IA. Vamos explorar os dois métodos mais comuns:

Normalização Min-Max#

Redimensiona os dados para um intervalo fixo, geralmente \([0, 1]\).

\[ x' = \frac{x - x_{min}}{x_{max} - x_{min}} \]

Padronização (Z-score)#

Transforma os dados para terem média 0 e desvio padrão 1, sendo mais robusta a valores discrepantes (outliers).

\[ x' = \frac{x - \mu}{\sigma} \]

Exemplo Prático: Comparando Técnicas

Baixar sun.jpeg

import matplotlib.pyplot as plt
import numpy as np
import cv2

# Carregar e preparar a imagem
img = cv2.imread('sun.jpeg', cv2.IMREAD_GRAYSCALE)

# 1. Normalização Min-Max [0, 1]
img_min_max = (img - img.min()) / (img.max() - img.min())

# 2. Padronização (Z-score)
img_z_score = (img - img.mean()) / img.std()

# Exibir resultados
plt.figure(figsize=(15, 5))

plt.subplot(1, 3, 1)
plt.imshow(img, cmap='gray')
plt.title(f'Original\nRange: [{img.min()}, {img.max()}]')
plt.axis('off')

plt.subplot(1, 3, 2)
plt.imshow(img_min_max, cmap='gray')
plt.title(f'Min-Max [0, 1]\nμ={img_min_max.mean():.2f}')
plt.axis('off')

plt.subplot(1, 3, 3)
plt.imshow(img_z_score, cmap='gray')
plt.title(f'Z-score\nμ={img_z_score.mean():.2f}, σ={img_z_score.std():.2f}')
plt.axis('off')

plt.tight_layout()
plt.show()

Histograma de Imagens#

O histograma é um gráfico que representa a distribuição das intensidades dos pixels em uma imagem. Ele nos diz quantos pixels existem para cada tom (ex: de 0 a 255).

Definição Matemática:

\[ P(k) = \frac{n_k}{N} \]

Onde \(n_k\) é o número de pixels com intensidade \(k\) e \(N\) é o total de pixels.

import numpy as np
import matplotlib.pyplot as plt
import cv2

# Carregar em escala de cinza
img = cv2.imread('sun.jpeg', cv2.IMREAD_GRAYSCALE)

# Calcular histograma
hist = cv2.calcHist([img], [0], None, [256], [0, 256])

# Visualizar
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.imshow(img, cmap='gray')
plt.title('Imagem Original')
plt.axis('off')

plt.subplot(1, 2, 2)
plt.plot(hist, color='black')
plt.title('Histograma de Intensidades')
plt.xlabel('Tom (0-255)')
plt.ylabel('Frequência')
plt.show()

A função cv2.calcHist possui os seguintes parâmetros principais:

  • [img]: Imagem de entrada (deve estar entre colchetes).

  • [0]: Índice do canal (0 para tons de cinza).

  • None: Máscara para processar apenas parte da imagem.

  • [256]: Número de bins (quantidade de tons representados).

  • [0, 256]: Intervalo das intensidades (0 a 255).

Interpretação de Histogramas#

Padrão

Significado

Ação Sugerida

Picos Agudos

Grandes áreas uniformes (ex: céu ou paredes).

-

Faixa Estreita

Baixo contraste (imagem “lavada”).

Equalização ou CLAHE.

Deslocado à Esquerda

Imagem escura (subexposta).

Aumentar brilho / Ganho.

Deslocado à Direita

Imagem muito clara (superexposta).

Reduzir brilho / Exposição.

Acumulado nos Cantos

Perda de detalhes nos pretos ou brancos.

Ajuste de Curvas ou HDR.

Bimodal

Objeto claro sobre fundo escuro (ou vice-versa).

Segmentação por Limiar.

Equalização de Histograma#

A equalização é uma técnica de processamento de imagem que visa aumentar o contraste global ao redistribuir as intensidades dos pixels. O objetivo é que o histograma da imagem resultante seja o mais uniforme possível, aproveitando melhor toda a faixa dinâmica disponível (ex: de 0 a 255).

Como funciona?#

O processo se baseia na Função de Distribuição Cumulativa (CDF). Imagine que a CDF nos diz a probabilidade de um pixel ter intensidade menor ou igual a um valor \(r_k\). Na equalização, usamos essa probabilidade para mapear os tons originais para novos valores:

  • Mapeamento: \(s_k = T(r_k) = (L-1) \cdot \text{CDF}(r_k)\)

  • Intuição: Áreas da imagem que concentram muitos pixels em poucos tons são “esticadas”, enquanto áreas com poucos pixels são comprimidas. Isso faz com que detalhes escondidos em sombras ou luzes muito fortes se tornem visíveis.

import cv2
import matplotlib.pyplot as plt

# Carregar imagem original
img = cv2.imread('sun.jpeg', cv2.IMREAD_GRAYSCALE)

# Aplicar equalização global
img_eq = cv2.equalizeHist(img)

# Configurar visualização 2x2
fig, axs = plt.subplots(2, 2, figsize=(12, 8))

# Linha 1: Imagens
axs[0, 0].imshow(img, cmap='gray')
axs[0, 0].set_title('Original')
axs[0, 0].axis('off')

axs[0, 1].imshow(img_eq, cmap='gray')
axs[0, 1].set_title('Após Equalização')
axs[0, 1].axis('off')

# Linha 2: Histogramas
axs[1, 0].hist(img.ravel(), 256, [0, 256], color='black')
axs[1, 0].set_title('Histograma Original')

axs[1, 1].hist(img_eq.ravel(), 256, [0, 256], color='black')
axs[1, 1].set_title('Histograma Equalizado')

plt.tight_layout()
plt.show()

CLAHE: Equalização Adaptativa#

Enquanto a equalização global utiliza a mesma transformação para a imagem inteira, o CLAHE (Contrast Limited Adaptive Histogram Equalization) adota uma abordagem local. Ele divide a imagem em pequenas regiões chamadas tiles (geralmente 8x8 pixels) e aplica a equalização em cada uma delas separadamente.

Por que usar o CLAHE?

  • Detalhes Locais: Melhora o contraste em áreas específicas sem distorcer o brilho global da imagem.

  • Limite de Ruído: O mecanismo de “Limite de Contraste” corta picos muito altos no histograma local, evitando a amplificação excessiva de ruídos.

  • Naturalidade: Produz resultados mais equilibrados e visualmente agradáveis do que a equalização comum.

Experimento Interativo: Equalização e Histograma#

Utilize o playground abaixo para carregar uma imagem e ajustar o contraste usando o CLAHE. O slider controla o limite de contraste (Clip Limit), permitindo observar como o histograma se redistribui em tempo real.

import gradio as gr
import cv2
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

def interactive_equalization(input_img, clip_limit):
    # Converter para escala de cinza
    img = np.array(input_img)
    if len(img.shape) == 3:
        img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    
    # Aplicar CLAHE
    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(8, 8))
    img_clahe = clahe.apply(img)
    
    # Gerar Comparação de Histogramas
    fig, axs = plt.subplots(1, 2, figsize=(10, 4))
    
    axs[0].hist(img.flatten(), bins=256, range=[0, 256], color='gray', alpha=0.7)
    axs[0].set_title("Histograma Original")
    
    axs[1].hist(img_clahe.flatten(), bins=256, range=[0, 256], color='blue', alpha=0.7)
    axs[1].set_title(f"Histograma CLAHE (Clip={clip_limit})")
    
    plt.tight_layout()
    plt.close() # Evita exibição dupla no notebook
    
    return img_clahe, fig

demo = gr.Interface(
    fn=interactive_equalization,
    inputs=[
        gr.Image(type="pil", label="Upload Imagem"),
        gr.Slider(1, 10, value=2, label="Limite de Contraste (Clip Limit)")
    ],
    outputs=[
        gr.Image(label="Resultado CLAHE"),
        gr.Plot(label="Comparação de Histogramas")
    ],
    title="Playground de Equalização Adaptativa"
)

# demo.launch()

Desafio de Aprimoramento: Tente modificar o código acima para adicionar uma nova funcionalidade simples, como:

  • Um slider para ajustar o brilho da imagem antes da equalização.

  • Um botão para alternar entre a Equalização Global (cv2.equalizeHist) e o CLAHE.

Convolução de Imagens#

A convolução (vid) é uma operação matemática central no processamento digital de imagens. Seu principal objetivo é extrair e realçar características relevantes — como bordas, texturas, contornos ou regiões homogêneas — ao combinar uma imagem com um kernel (ou filtro). O resultado é uma nova imagem, cujas propriedades visuais são modificadas de acordo com a estrutura do kernel utilizado.

Matemática da Convolução#

Para imagens digitais, representadas como matrizes bidimensionais de pixels, a convolução discreta é definida por:

\[(f * g)[x,y] = \sum_{i=-k}^{k} \sum_{j=-k}^{k} f[x-i,y-j] \cdot g[i,j] \]

Onde:

  • \(*\): Operador de convolução

  • \(f\): Matriz da imagem original (dimensão M×N)

  • \(g\): Kernel ou filtro (dimensão K×K, com K ímpar, como 3×3 ou 5×5)

  • \((x,y)\): Coordenadas do pixel na imagem de saída

  • \(k\): Metade do tamanho do kernel, ou seja, \((K-1)/2\), necessário para centralizar o kernel sobre o pixel atual

Esse processo é repetido para cada pixel da imagem, levando em conta a vizinhança local e os coeficientes definidos pelo kernel.

Kernels: O Coração da Convolução#

Os kernels são pequenas matrizes de pesos que determinam como os valores dos pixels vizinhos devem ser combinados para gerar um novo valor. Suas propriedades determinam o tipo de transformação que será aplicada à imagem.

Propriedade

Descrição

Dimensões Ímpares

Garantem que o kernel tenha um centro bem definido para alinhar ao pixel sendo processado.

Normalização e Balanceamento

Preserva o brilho da imagem (soma dos pesos = 1) ou enfatiza transições com pesos positivos e negativos.

Direcionalidade e Gradientes

Detectam variações horizontais, verticais ou diagonais — úteis para encontrar bordas.

Objetivo Específico

Cada kernel serve a uma tarefa: suavizar, realçar, detectar ruído, etc.

Localidade e Paralelismo

Operação local e independente — ideal para paralelismo em GPUs e redes neurais convolucionais.

Exemplo de Kernel de Detecção de Bordas (Sobel Vertical):#

\[\begin{split}\begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix} \end{split}\]

Esse kernel detecta bordas verticais ao responder a variações horizontais na imagem. Parece um pouco abstrato mas entenderemos em seguida.

Catálogo de Kernels Úteis#

Cada kernel foi projetado para uma tarefa específica. A tabela abaixo reúne os principais filtros usados em visão computacional, descrevendo o efeito produzido e a intuição por trás de seus coeficientes.

Kernel

Aplicação

Intuição

Identidade

Saída idêntica à entrada

Centro = 1, todo o restante = 0: passa apenas o pixel atual sem misturar vizinhos.

Média (Box)

Suavização uniforme

Todos os pesos iguais a 1/9: tira a média da janela — borra detalhes e reduz ruído.

Gaussiano

Suavização natural

Pesos maiores no centro (1/16, 2/16, 4/16): suavização gradual que preserva melhor as bordas que o Box.

Sobel Horizontal

Detecção de bordas horizontais

Pesos opostos no topo (−) e na base (+): responde a variações verticais de intensidade.

Sobel Vertical

Detecção de bordas verticais

Pesos opostos na esquerda (−) e na direita (+): responde a variações horizontais de intensidade.

Laplaciano

Bordas em todas as direções

Centro negativo (−4) rodeado de positivos: mede a curvatura local — zero em regiões planas, alto nas bordas.

Nitidez (Sharpen)

Realce de detalhes

Centro amplificado (5) com vizinhos negativos (−1): acentua diferenças entre o pixel e seus vizinhos.

Realce de Bordas

Bordas com alto contraste

Centro = 8, todos os vizinhos = −1: versão mais agressiva do Laplaciano — bordas ficam muito marcadas.

Visualização dos kernels úteis

Convolução Passo a Passo: Suavização com Filtro de Média#

Suponha uma imagem 5×5 e um kernel de média 3×3. A janela do kernel desliza pela imagem, posicionando-se em cada pixel válido e realizando multiplicações ponto a ponto seguidas de uma soma. O resultado de cada etapa preenche uma posição na matriz de saída (3×3 no modo valid).

Imagem A (5×5) e Kernel B (3×3):

\[\begin{split} A = \begin{bmatrix} 10 & 20 & 30 & 40 & 50 \\ 20 & 30 & 40 & 50 & 60 \\ 30 & 40 & 50 & 60 & 70 \\ 40 & 50 & 60 & 70 & 80 \\ 50 & 60 & 70 & 80 & 90 \end{bmatrix} \qquad B = \frac{1}{9}\begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{bmatrix} \end{split}\]

Etapa 1 — Posição de saída \(C[0,0]\)#

O kernel é posicionado no canto superior esquerdo, cobrindo \(A[0{:}3,\, 0{:}3]\):

\[\begin{split} \begin{bmatrix} \boxed{10} & \boxed{20} & \boxed{30} & 40 & 50 \\ \boxed{20} & \boxed{30} & \boxed{40} & 50 & 60 \\ \boxed{30} & \boxed{40} & \boxed{50} & 60 & 70 \\ 40 & 50 & 60 & 70 & 80 \\ 50 & 60 & 70 & 80 & 90 \end{bmatrix} \end{split}\]
\[ C[0,0] = \frac{1}{9}(10+20+30+20+30+40+30+40+50) = \frac{270}{9} = \mathbf{30} \]

Matriz de saída parcial:

\[\begin{split} C = \begin{bmatrix} \mathbf{30} & ? & ? \\ ? & ? & ? \\ ? & ? & ? \end{bmatrix} \end{split}\]

Etapa 2 — Posição de saída \(C[0,1]\)#

O kernel desliza uma coluna à direita, cobrindo \(A[0{:}3,\, 1{:}4]\):

\[\begin{split} \begin{bmatrix} 10 & \boxed{20} & \boxed{30} & \boxed{40} & 50 \\ 20 & \boxed{30} & \boxed{40} & \boxed{50} & 60 \\ 30 & \boxed{40} & \boxed{50} & \boxed{60} & 70 \\ 40 & 50 & 60 & 70 & 80 \\ 50 & 60 & 70 & 80 & 90 \end{bmatrix} \end{split}\]
\[ C[0,1] = \frac{1}{9}(20+30+40+30+40+50+40+50+60) = \frac{360}{9} = \mathbf{40} \]

Matriz de saída parcial:

\[\begin{split} C = \begin{bmatrix} 30 & \mathbf{40} & ? \\ ? & ? & ? \\ ? & ? & ? \end{bmatrix} \end{split}\]

Etapa 3 — Posição de saída \(C[1,1]\) (centro)#

Após percorrer toda a primeira linha e avançar uma linha, o kernel cobre \(A[1{:}4,\, 1{:}4]\):

\[\begin{split} \begin{bmatrix} 10 & 20 & 30 & 40 & 50 \\ 20 & \boxed{30} & \boxed{40} & \boxed{50} & 60 \\ 30 & \boxed{40} & \boxed{50} & \boxed{60} & 70 \\ 40 & \boxed{50} & \boxed{60} & \boxed{70} & 80 \\ 50 & 60 & 70 & 80 & 90 \end{bmatrix} \end{split}\]
\[ C[1,1] = \frac{1}{9}(30+40+50+40+50+60+50+60+70) = \frac{450}{9} = \mathbf{50} \]

Matriz de saída parcial:

\[\begin{split} C = \begin{bmatrix} 30 & 40 & ? \\ ? & \mathbf{50} & ? \\ ? & ? & ? \end{bmatrix} \end{split}\]

Resultado Final — Todas as 9 posições preenchidas#

Repetindo o processo para todas as posições válidas, a matriz de saída completa é:

\[\begin{split} C = \begin{bmatrix} 30 & 40 & 50 \\ 40 & 50 & 60 \\ 50 & 60 & 70 \end{bmatrix} \end{split}\]

A imagem resultante é 3×3 (modo valid), pois a janela 3×3 cabe em apenas 3×3 posições dentro de uma imagem 5×5.

Efeitos de Borda e Estratégias de Padding#

Durante a convolução, nas bordas da imagem faltam vizinhos para o kernel. Para corrigir isso, se for de interesse, usamos técnicas de padding.

Modos Comuns de Padding#

Modo

Descrição

Efeito no Tamanho

Frequência de Uso

Onde Você Mais Verá

same

Preenchimento com zeros

Mantém o tamanho original

Altíssima

CNNs modernas (YOLO, Transformers)

valid

Sem preenchimento

Reduz a imagem

Alta

Redução de dimensionalidade controlada

reflect

Espelha os valores da borda

Mantém o tamanho

Moderada

Fotografia digital, imagens de satélite e médicas

replicate

Repete o pixel da borda

Mantém o tamanho

Baixa

Filtros simples de borda e otimizações de hardware

Cálculo da Dimensão de Saída#

A fórmula responde à pergunta: quantas vezes a janela do kernel cabe na imagem? Após subtrair o tamanho do kernel (\(I - K\)) e adicionar o padding de ambos os lados (\(+2P\)), dividimos pelo passo (\(S\)) e somamos 1 pela posição inicial.

Símbolo

Significado

Valor típico

\(I\)

Tamanho da imagem original (largura ou altura)

ex: 224

\(K\)

Tamanho do kernel

3 ou 5

\(P\)

Padding adicionado em cada lado

0 ou 1

\(S\)

Stride — passo da janela entre posições

1 ou 2

\(O\)

Tamanho da imagem de saída

calculado

\[ O = \left\lfloor \frac{I - K + 2P}{S} \right\rfloor + 1 \]

Sobre o stride: quando \(S = 1\) (padrão), o kernel avança pixel a pixel. Com \(S = 2\), ele pula uma posição a cada passo — reduzindo a saída pela metade. CNNs usam stride > 1 para fazer downsampling sem pooling.

Resumo por modo (com \(I=5\), \(K=3\), \(S=1\)):

Modo

\(P\)

Cálculo

Saída \(O\)

valid

\(0\)

\(\lfloor(5-3+0)/1\rfloor+1\)

3

same

\(1\)

\(\lfloor(5-3+2)/1\rfloor+1\)

5

reflect

\(1\)

\(\lfloor(5-3+2)/1\rfloor+1\)

5

Exemplo passo a passo — Modo valid

Para \(I=5\) e \(K=3\), a janela do kernel desliza pela imagem com passo \(S=1\). A ilustração abaixo mostra as 3 posições horizontais válidas na primeira linha — o kernel para quando ultrapassaria o limite da imagem. O mesmo raciocínio se aplica às linhas: 3 posições × 3 posições = saída 3×3.

Deslizamento do kernel

Confirmando com a fórmula:

\[ O = \left\lfloor \frac{5 - 3 + 2 \cdot 0}{1} \right\rfloor + 1 = \left\lfloor \frac{2}{1} \right\rfloor + 1 = 2 + 1 = \mathbf{3} \]

Exemplo passo a passo — Modo same

Para manter \(O = I\), calcula-se o padding necessário: \(P = \lfloor K/2 \rfloor = \lfloor 3/2 \rfloor = 1\).

\[ O = \left\lfloor \frac{5 - 3 + 2 \cdot 1}{1} \right\rfloor + 1 = \left\lfloor \frac{4}{1} \right\rfloor + 1 = 4 + 1 = \mathbf{5} \]

Com 1 pixel de borda adicionado em cada lado, a saída mantém o tamanho original: 5×5.

Exemplo Visual: Comparação de Modos

Mesma imagem e kernel para os três modos, mostrando o padding aplicado e a saída completa.

\[\begin{split} A = \begin{bmatrix} 10 & 20 & 30 \\ 40 & 50 & 60 \\ 70 & 80 & 90 \end{bmatrix} \qquad B = \frac{1}{9}\begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{bmatrix} \end{split}\]

valid — sem padding → saída 1×1

Com kernel 3×3 sobre uma imagem 3×3, há apenas uma posição onde o kernel cabe completamente: sobre a imagem inteira.

\[ C = \begin{bmatrix} 50.0 \end{bmatrix} \]

same — zero padding → saída 3×3

Uma borda de zeros é adicionada em todos os lados:

\[\begin{split} A_{\text{pad}} = \begin{bmatrix} 0 & 0 & 0 & 0 & 0 \\ 0 & 10 & 20 & 30 & 0 \\ 0 & 40 & 50 & 60 & 0 \\ 0 & 70 & 80 & 90 & 0 \\ 0 & 0 & 0 & 0 & 0 \end{bmatrix} \end{split}\]

Resultado completo:

\[\begin{split} C_{\text{same}} = \begin{bmatrix} 13.3 & 23.3 & 17.8 \\ 30.0 & 50.0 & 36.7 \\ 26.7 & 43.3 & 31.1 \end{bmatrix} \end{split}\]

Os valores das bordas ficam atenuados pois os zeros do padding reduzem a média local.


reflect — espelhamento → saída 3×3

A borda é preenchida com o reflexo dos pixels vizinhos (sem repetir o pixel da borda):

\[\begin{split} A_{\text{pad}} = \begin{bmatrix} 50 & 40 & 50 & 60 & 50 \\ 20 & 10 & 20 & 30 & 20 \\ 50 & 40 & 50 & 60 & 50 \\ 80 & 70 & 80 & 90 & 80 \\ 50 & 40 & 50 & 60 & 50 \end{bmatrix} \end{split}\]

Resultado completo:

\[\begin{split} C_{\text{reflect}} = \begin{bmatrix} 36.7 & 40.0 & 43.3 \\ 46.7 & 50.0 & 53.3 \\ 56.7 & 60.0 & 63.3 \end{bmatrix} \end{split}\]

Os valores das bordas ficam próximos ao conteúdo real — o espelhamento preserva a continuidade local.


Comparação dos resultados:

Posição

valid

same

reflect

Canto superior esquerdo

13.3

36.7

Centro

50.0

50.0

50.0

Canto inferior direito

31.1

63.3

O centro é sempre 50.0 independente do modo — é o único pixel que nunca toca a borda de padding.

Aplicações Práticas com Python#

Com os conceitos estabelecidos, é hora de implementar. Esta seção está organizada em duas abordagens complementares:

Abordagem

Quando usar

OpenCV

Imagens reais, produção, performance

NumPy + scipy

Kernels personalizados, fins didáticos, experimentação


Verificação com as matrizes da teoria

Antes de partir para imagens reais, confirmamos o que calculamos manualmente: a convolução da matriz \(A\) (5×5) com o kernel de média \(B\) (3×3) no modo valid deve produzir a saída \(C\) (3×3) com os valores que já conhecemos.

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import convolve2d

# Define a matriz da imagem
A = np.array([[10, 20, 30, 40, 50],
       [20, 30, 40, 50, 60],
       [30, 40, 50, 60, 70],
       [40, 50, 60, 70, 80],
       [50, 60, 70, 80, 90]])

# Define o kernel de média
B = np.array([[1/9, 1/9, 1/9],
       [1/9, 1/9, 1/9],
       [1/9, 1/9, 1/9]])

# Realiza a convolução usando a função convolve2d do scipy
C = convolve2d(A, B, mode='valid')

# Subplots com aspect ratio
fig, axes = plt.subplots(1, 3, figsize=(16, 6), 
             gridspec_kw={'width_ratios': [5, 3, 3]}) # Proporção dos tamanhos

# Imagem original
im1 = axes[0].imshow(A, cmap='gray', aspect='equal')
axes[0].set_title(f'Imagem Original ({A.shape[0]}x{A.shape[1]})')
#fig.colorbar(im1, ax=axes[0], fraction=0.046, pad=0.04)

# Kernel - respeitando a proporção
im2 = axes[1].imshow(B, cmap='gray', aspect='equal')
axes[1].set_title(f'Kernel ({B.shape[0]}x{B.shape[1]})')
axes[1].set_xticks([]) # Remove os valores do eixo x
axes[1].set_yticks([]) # Remove os valores do eixo y
#fig.colorbar(im2, ax=axes[1], fraction=0.046, pad=0.04)

# Imagem convolucionada
im3 = axes[2].imshow(C, cmap='gray', aspect='equal')
axes[2].set_title(f'Imagem Convolucionada ({C.shape[0]}x{C.shape[1]})')
axes[2].set_xticks([]) # Remove os valores do eixo x
axes[2].set_yticks([]) # Remove os valores do eixo y
#fig.colorbar(im3, ax=axes[2], fraction=0.046, pad=0.04)

plt.tight_layout()
plt.show()

A saída \(C\) bate exatamente com os valores calculados no passo a passo anterior — confirmando que a teoria e o código estão alinhados.

Exemplo de convolução

🎬 Exercício — Animação do Processo de Convolução

Você viu o processo de convolução acontecer passo a passo. Agora, que tal torná-lo visível de forma animada?

Seu desafio: usando matplotlib.animation, crie um vídeo que mostre o kernel de média 3×3 deslizando sobre a matriz \(A\) (5×5) no modo valid. A cada quadro da animação, destaque:

  • A região da imagem coberta pelo kernel (retângulo colorido)

  • Os valores sendo multiplicados

  • O valor resultante sendo preenchido na matriz de saída \(C\)

Dicas:

  • Use FuncAnimation do módulo matplotlib.animation

  • Para salvar como vídeo .mp4, instale o ffmpeg: !apt-get install ffmpeg -y

  • Para exibir no notebook sem salvar, use HTML(anim.to_jshtml())

from matplotlib.animation import FuncAnimation
from IPython.display import HTML

🔹 Com OpenCV (Funções prontas e otimizadas)#

O OpenCV oferece funções de convolução altamente otimizadas, com suporte a aceleração por hardware e integração direta com formatos de imagem reais. Em vez de implementar manualmente o deslizamento do kernel, utilizamos funções dedicadas para cada tipo de filtro — o que torna o código mais conciso, robusto e adequado para uso em produção.

A seguir, exploramos três filtros clássicos com aplicações distintas:

Filtro

Função OpenCV

Efeito principal

Gaussiano

cv2.GaussianBlur

Suavização — reduz ruído e detalhes finos

Sobel

cv2.Sobel

Detecção de bordas — realça gradientes de intensidade

Mediana

cv2.medianBlur

Remoção de ruído impulsivo (sal e pimenta)

Para os exemplos, usaremos a imagem a seguir — baixe aqui antes de executar o código.

Suavização com Filtro Gaussiano

import cv2
import matplotlib.pyplot as plt

# Carrega a imagem
img = cv2.imread('bridge.jpeg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# Número de iterações do filtro Gaussiano
num_iterations = 5 # Altere conforme desejar

# Cria uma cópia da imagem para aplicar o filtro iterativamente
blurred_img = img.copy()

for _ in range(num_iterations):
  blurred_img = cv2.GaussianBlur(blurred_img, (5, 5), 0) #img, kernel, std

# Plota apenas a original e a última versão borrada
plt.figure(figsize=(10, 5))

plt.subplot(1, 2, 1)
plt.imshow(img)
plt.title("Imagem Original")
plt.axis('off')

plt.subplot(1, 2, 2)
plt.imshow(blurred_img)
plt.title(f"Filtro Gaussiano ({num_iterations}x)")
plt.axis('off')

plt.tight_layout()
plt.show()

Explicação:

  • cv2.GaussianBlur(src, ksize, sigmaX) aplica uma suavização com base em uma distribuição gaussiana.

    • src: imagem original.

    • ksize=(5,5): tamanho da máscara (kernel). Quanto maior, mais borrado o resultado.

    • sigmaX=0: o desvio padrão da Gaussiana. Zero permite que o OpenCV calcule automaticamente com base no tamanho do kernel.

  • Aqui, aplicamos o filtro várias vezes para reforçar o efeito.

  • Útil para remover ruídos leves e suavizar gradientes antes de aplicar detecção de bordas.

Detecção de Bordas com Sobel

import cv2
import numpy as np
import matplotlib.pyplot as plt

# Carrega a imagem original
img = cv2.imread('bridge.jpeg', cv2.IMREAD_GRAYSCALE) # Sobel funciona melhor em tons de cinza

# Aplica Sobel nos dois eixos
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
sobel_combined = cv2.magnitude(sobel_x, sobel_y)

# Aplica Gaussiano DEPOIS (opcional para suavizar ruído nas bordas)
blurred_edges = cv2.GaussianBlur(sobel_combined, (5,5), 0)

# Plota os resultados
fig, axs = plt.subplots(1, 3, figsize=(15,5))
axs[0].imshow(img, cmap='gray')
axs[0].set_title("Original")
axs[0].axis('off')

axs[1].imshow(sobel_combined, cmap='gray')
axs[1].set_title("Sobel Puro")
axs[1].axis('off')

axs[2].imshow(blurred_edges, cmap='gray')
axs[2].set_title("Sobel + Gaussiano")
axs[2].axis('off')

plt.tight_layout()
plt.show()

Explicação:

  • cv2.Sobel(src, ddepth, dx, dy, ksize) calcula derivadas para encontrar bordas.

    • ddepth=cv2.CV_64F: profundidade dos dados de saída (64 bits flutuante para evitar perda de sinal negativo).

    • dx=1, dy=0: para detectar bordas horizontais (vice-versa para verticais).

    • ksize=3: tamanho do kernel de derivada.

  • cv2.magnitude(x, y) combina as bordas em X e Y para formar uma borda geral.

  • Aplicar o filtro Gaussiano depois ajuda a suavizar ruídos nas bordas detectadas.

Redução de Ruído com Filtro Mediano

import cv2
import numpy as np
import matplotlib.pyplot as plt

# Carregar imagem original
img = cv2.imread('bridge.jpeg') # Substitua pelo seu arquivo
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# Adicionar ruído artificial (sal e pimenta)
def add_salt_pepper_noise(image, prob=0.05):
  output = np.copy(image)
  # Ruído sal (branco)
  salt = np.random.rand(*image.shape[:2]) < prob/2
  output[salt] = 255
  # Ruído pimenta (preto)
  pepper = np.random.rand(*image.shape[:2]) < prob/2
  output[pepper] = 0
  return output

noisy_img = add_salt_pepper_noise(img, prob=0.3) # 30% de ruído

# Aplicar filtro mediano
median = cv2.medianBlur(noisy_img, 5) # Kernel size 5 (deve ser ímpar)

# Visualização comparativa
fig, axs = plt.subplots(1, 2, figsize=(12, 6))

axs[0].imshow(noisy_img)
axs[0].set_title(f"Imagem com Ruído (30% sal-pimenta)")
axs[0].axis('off')

axs[1].imshow(median)
axs[1].set_title("Filtro Mediano (kernel=5)")
axs[1].axis('off')

plt.tight_layout()
plt.show()

Explicação:

  • cv2.medianBlur(src, ksize) aplica um filtro que substitui cada pixel pela mediana da vizinhança.

    • Muito eficaz contra ruídos do tipo sal e pimenta (pontinhos pretos e brancos aleatórios).

    • ksize deve ser ímpar (3, 5, 7…).

  • Aqui, um ruído artificial foi adicionado para mostrar a eficácia do filtro.

  • Dica: para preservar bordas, o filtro mediano é geralmente melhor que o Gaussiano.

Resultado filtro mediano

🔹 Com NumPy (Definindo o Kernel Manualmente)#

Utilizando scipy.signal.convolve2d para aplicar filtros 2D com controle total, ideal para fins didáticos e experimentação com kernels personalizados.

Filtro de Média (Suavização Simples)

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import convolve2d

# Carregar imagem em escala de cinza
img = plt.imread('bridge.jpeg')
img = img.mean(axis=2) # Converter para escala de cinza

# Define o kernel de média (suavização simples)
kernel = np.ones((3, 3)) / 9

# Aplica a convolução
output = img.copy()

# Aplica o filtro n vezes: cada passagem suaviza mais
# (equivalente a aumentar o kernel, útil para visualizar o efeito progressivo)
n = 10
for i in range(n):
  output = convolve2d(output, kernel, mode='same', boundary='fill', fillvalue=0)

# Visualização
fig, axs = plt.subplots(1, 2, figsize=(10, 4))
axs[0].imshow(img, cmap='gray')
axs[0].set_title("Imagem Original (Gray)")
axs[0].axis('off')

axs[1].imshow(output, cmap='gray')
axs[1].set_title("Filtro de Média (3x3)")
axs[1].axis('off')

plt.tight_layout()
plt.show()

Explicação:

  • np.ones((3,3))/9 cria um kernel onde todos os elementos têm peso igual — média aritmética dos vizinhos.

  • convolve2d(img, kernel, mode='same', boundary='fill', fillvalue=0):

    • mode='same': saída com o mesmo tamanho da imagem original.

    • boundary='fill': bordas externas tratadas como zero.

  • Suaviza transições na imagem, útil para remover ruídos leves.

Detecção de Bordas com Filtro Laplaciano

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import convolve2d

# Carregar imagem em escala de cinza
img = plt.imread('bridge.jpeg')
if img.ndim == 3: # Se for colorida (RGB)
  img = img.mean(axis=2) # Converter para escala de cinza

# Define o kernel Laplaciano
kernel = np.array([[0, 1, 0],
          [1, -4, 1],
          [0, 1, 0]])

# Aplica convolução
output = convolve2d(img, kernel, mode='same', boundary='fill', fillvalue=0)

# Pré-processamento para visualização
output_visual = np.abs(output) # Valor absoluto para ver todas as bordas
output_visual = (output_visual - output_visual.min()) / (output_visual.max() - output_visual.min()) # Normaliza para [0,1]

# Visualização melhorada
fig, axs = plt.subplots(1, 3, figsize=(15, 5))

# Imagem original
axs[0].imshow(img, cmap='gray')
axs[0].set_title("Imagem Original")
axs[0].axis('off')

# Resultado Laplaciano (valores brutos)
axs[1].imshow(output, cmap='seismic', vmin=-100, vmax=100) # Cores: vermelho=negativo, azul=positivo
axs[1].set_title("Laplaciano (valores reais)")
axs[1].axis('off')

# Resultado Laplaciano (absoluto normalizado)
axs[2].imshow(output_visual, cmap='gray')
axs[2].set_title("Laplaciano (absoluto normalizado)")
axs[2].axis('off')

plt.tight_layout()
plt.show()

Explicação:

  • O kernel realça regiões com mudança brusca de intensidade (bordas).

  • O centro negativo e vizinhos positivos atuam como uma segunda derivada discreta.

  • Ideal para detectar bordas finas e simétricas.

Resultado filtro laplaciano

CNNs#

Daqui a pouco estudaremos as CNNs (Convolução em Redes Neurais), é importante saber que os kernels são aprendidos automaticamente durante o treinamento. Cada camada convolucional extrai características específicas e cada vez mais complexas da imagem:

Camada

Tamanho do Kernel

Nº de Filtros

Função da Camada

1

3×3

32

Bordas e cores básicas

2

5×5

64

Padrões e texturas

3

3×3

128

Formas complexas e objetos

Esses filtros tornam possíveis aplicações como reconhecimento facial, análise médica, e detecção de objetos em tempo real.

(Exercício Prático) Processamento de Vídeo com OpenCV#

Objetivo: Aprender a carregar, modificar e salvar vídeos usando a biblioteca OpenCV.

Tarefas:

  • Carregar um vídeo (de arquivo ou webcam).

  • Converter cada frame para escala de cinza.

  • Salvar o vídeo processado em um novo arquivo.

Código Base (Passo a Passo):

import cv2

# 1. Carregar vídeo (substitua 'video.mp4' pelo seu arquivo ou use 0 para webcam)
cap = cv2.VideoCapture('video.mp4')

# Verifica se o vídeo foi carregado corretamente
if not cap.isOpened():
  print("Erro ao abrir o vídeo!")
  exit()

# 2. Obter propriedades do vídeo (largura, altura e FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)

# 3. Criar o objeto de escrita do novo vídeo (grayscale)
output = cv2.VideoWriter('video_out.mp4',
             cv2.VideoWriter_fourcc(*'mp4v'), # Codec MP4
             fps,
             (width, height),
             isColor=False) # Vídeo em tons de cinza

# 4. Processar frame a frame, aplicar filtros, etc
# ...

# 5. Liberar recursos
cap.release()
output.release()
cv2.destroyAllWindows()

print("Vídeo processado e salvo como 'video_out.mp4'!")

Próximos Desafios (opcional):

  • Aplique equalização de histograma em cada frame antes de salvar.

  • Escolha e aplique um filtro (mediana, gaussiano, bordas etc).

  • Crie um vídeo que combine frames originais e processados lado a lado.

Desafio:

  • Criar um gerador de filtros adaptativo e especializado para CNNs. Falar com o professor!

Exercícios Conceituais#

1. Importância do Pré-processamento de Imagens Explique por que o pré-processamento é uma etapa essencial em projetos de visão computacional.

  • Quais problemas podem surgir ao utilizar imagens brutas sem pré-processamento?

  • Cite algumas técnicas comuns de pré-processamento e suas finalidades.

2. Normalização de Intensidade A normalização de intensidade ajusta os valores dos pixels para uma faixa específica.

  • Descreva como a normalização min-max transforma os valores de uma imagem.

  • Em quais situações essa técnica é particularmente útil?

3. Equalização de Histograma A equalização de histograma redistribui os níveis de intensidade para melhorar o contraste da imagem.

  • Como essa técnica pode realçar detalhes em imagens com baixa variação tonal?

  • Quais são as possíveis limitações da equalização de histograma?

4. Filtros Espaciais: Média, Mediana e Gaussiano Compare os filtros de média, mediana e Gaussiano em termos de suas aplicações e efeitos nas imagens.

  • Qual filtro seria mais adequado para remover ruídos do tipo “sal e pimenta”?

  • Em que casos o filtro Gaussiano é preferido?

5. Realce de Bordas em Imagens O realce de bordas é fundamental para destacar transições abruptas de intensidade.

  • Quais operadores são comumente utilizados para essa finalidade?

  • Como o realce de bordas contribui para a segmentação de objetos em uma imagem?

6. Redimensionamento de Imagens Redimensionar imagens para dimensões fixas é uma prática comum em redes neurais convolucionais (CNNs).

  • Quais são os desafios ao redimensionar imagens sem distorcer informações importantes?

  • Como o redimensionamento afeta o desempenho de modelos de aprendizado de máquina?

7. Convolução e Kernels A convolução é uma operação fundamental em processamento de imagens.

  • Explique como os kernels (ou filtros) são utilizados na convolução para detectar características específicas na imagem.

  • Dê exemplos de kernels comuns e suas aplicações.

8. Padding em Convoluções O padding é uma técnica utilizada para lidar com as bordas durante a convolução.

  • Quais são os tipos comuns de padding e como eles influenciam o resultado da convolução?

  • Por que o uso adequado de padding é importante em redes neurais convolucionais?

Referências e Conteúdo Extra#