5  Transformadas e Compressão

“A Fourier transform does not change the information content of a signal; it changes the representation.” — R. C. Gonzalez & R. E. Woods

Nos capítulos anteriores, todas as operações foram realizadas no domínio espacial: filtros de convolução, morfologia e segmentação atuam diretamente sobre os valores de intensidade dos pixels. Este capítulo apresenta uma perspectiva complementar e poderosa — o domínio da frequência — que revela como a imagem é construída a partir de padrões oscilatórios de diferentes escalas.

A ideia central é simples: qualquer imagem pode ser decomposta em uma soma de senoides bidimensionais, cada uma com frequência, amplitude e fase próprias. Baixas frequências correspondem a variações suaves (fundo, iluminação global); altas frequências correspondem a bordas, texturas e ruído.

Esta decomposição abre três caminhos práticos:

  1. Filtragem espectral — suprimir ou realçar seletivamente faixas de frequência;
  2. Análise multi-escala com wavelets — capturar estruturas em diferentes resoluções e posições;
  3. Compressão — eliminar coeficientes imperceptíveis ao olho humano, reduzindo o volume de dados.

5.1 Objetivos

Ao concluir este capítulo, você será capaz de:

  • Interpretar o espectro de Fourier de uma imagem: distinguir magnitude de fase, localizar baixas e altas frequências, e ler o espectro de forma intuitiva;
  • Aplicar o Teorema da Convolução para implementar filtragem eficiente via FFT, compreendendo o ganho computacional em relação à convolução direta;
  • Projetar e comparar filtros espectrais — passa-baixa (Ideal, Gaussiano, Butterworth), passa-alta e notch — com foco nos seus efeitos visuais e nos artefatos que produzem;
  • Compreender wavelets e multirresolução: interpretar as subbandas LL/LH/HL/HH e relacionar a análise wavelet às camadas de CNNs;
  • Entender o pipeline JPEG: desde a DCT em blocos 8×8 até a quantização perceptual e os artefatos de compressão;
  • Escolher o formato de imagem adequado (JPEG, PNG, WebP) com base no conteúdo e na aplicação.

5.2 Configuração do Ambiente

import os, importlib, urllib.request
import numpy as np
import matplotlib.pyplot as plt
import cv2
from scipy import ndimage

BASE_URL = "https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph"
for f in ["morph.py"]:
    if not os.path.exists(f):
        urllib.request.urlretrieve(f"{BASE_URL}/{f}", f)

import morph
importlib.reload(morph)
from morph import mm

print("✅ Ambiente pronto")
✅ Ambiente pronto

5.3 Transformada de Fourier Discreta 2D

A análise de Fourier fundamenta-se em um princípio poderoso: qualquer sinal periódico admite representação como soma de senoides com diferentes frequências, amplitudes e fases. Ver exemplo em uma dimensão na Figura 5.1.

Figura 5.1: Decomposição de Fourier 1D: uma onda quadrada (linha tracejada) é aproximada pela soma das primeiras senoides (linhas coloridas). Quanto mais termos, melhor a aproximação.

Aplicada a imagens digitais, essa ideia transforma uma matriz de valores de intensidade \(f(x,y)\) em uma representação no domínio da frequência, revelando quais padrões oscilatórios compõem a cena visual.

5.3.1 Fenômeno: o que uma imagem “parece” no domínio da frequência?

Observe o que acontece quando se transforma uma fotografia: ela se transforma em um padrão radial brilhante ao centro, com energia decaindo progressivamente em direção às bordas. Esse padrão é o espectro de magnitude da imagem — uma radiografia da distribuição de frequências.

Região do espectro O que representa Exemplos visuais
Centro (baixas freq.) Variações lentas — iluminação, cor de fundo, forma global Gradientes suaves, blocos uniformes
Meio (médias freq.) Texturas, padrões repetitivos Tecidos, tijolos, grama
Bordas (altas freq.) Variações abruptas — contornos, ruído Bordas de objetos, grão de imagem

Intuição-chave: suprimir o centro do espectro remove a estrutura global e enfatiza bordas; suprimir as bordas suaviza a imagem e remove ruído. É precisamente isso que os filtros espectrais fazem — mas com precisão cirúrgica impossível no domínio espacial.

5.3.2 O Experimento da Grade: Construindo a Imagem do Zero

Antes de mergulhar nas fórmulas complexas, vamos fazer o caminho inverso (IDFT). O que acontece se tivermos um espectro completamente vazio (preto) e acendermos apenas um único ponto brilhante (um coeficiente de frequência)?

O resultado no espaço real é uma grade perfeitamente senoidal. A posição desse ponto no espectro define a direção e a frequência (espessura) dessa grade.

N_grid = 100
espectro_vazio = np.zeros((N_grid, N_grid), dtype=complex)

# Acendendo um único ponto (frequência) fora do centro
u0, v0 = 10, 5  
espectro_vazio[N_grid//2 - v0, N_grid//2 - u0] = 1000

# Retornando para o domínio espacial (IDFT)
onda_2d = np.real(np.fft.ifft2(np.fft.ifftshift(espectro_vazio)))

onda_vis = cv2.normalize(onda_2d, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
espectro_vis = cv2.normalize(np.abs(espectro_vazio), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)

# Destaque visual do ponto
espectro_color = cv2.cvtColor(espectro_vis, cv2.COLOR_GRAY2BGR)
cv2.circle(espectro_color, (N_grid//2 - u0, N_grid//2 - v0), 2, (0, 0, 255), -1)

mm.show([espectro_color, onda_vis], titles=["Espectro (1 ponto ativo)", "Onda 2D Resultante (IDFT)"], cols=2, figsize=(10, 4))
Figura 5.2: Toda frequência no espectro (ponto isolado) corresponde a uma onda senoidal 2D rotacionada no domínio espacial.

5.3.3 Definição Matemática

Dada uma imagem \(f(x,y)\) de dimensões \(M \times N\), sua Transformada de Fourier Discreta 2D (DFT) é definida como:

\[ F(u,v) = \sum_{x=0}^{M-1} \sum_{y=0}^{N-1} f(x,y)\, e^{-j2\pi\left(\frac{ux}{M}+\frac{vy}{N}\right)} \tag{5.1}\]

onde \(u = 0, 1, \ldots, M-1\) e \(v = 0, 1, \ldots, N-1\) são as frequências discretas nas direções horizontal e vertical, respectivamente. O exponencial complexo \(e^{-j2\pi(ux/M + vy/N)}\) representa uma oscilação bidimensional de frequência \((u/M, v/N)\) ciclos por pixel.

A Transformada Inversa de Fourier Discreta 2D (IDFT) recupera a imagem original a partir de seu espectro:

\[ f(x,y) = \frac{1}{MN} \sum_{u=0}^{M-1} \sum_{v=0}^{N-1} F(u,v)\, e^{j2\pi\left(\frac{ux}{M}+\frac{vy}{N}\right)} \tag{5.2}\]

5.3.4 As Funções de Base da DCT

A frequência espacial \((u/M,v/N)\) representa a taxa de variação espacial da intensidade ao longo da imagem.

Valores baixos correspondem a variações suaves e estruturas globais; valores altos correspondem a mudanças rápidas de intensidade, como bordas, texturas e detalhes finos.

Em termos discretos, os índices \((u,v)\) indicam quantos ciclos da oscilação ocorrem ao longo da largura e da altura da imagem.

Cada bloco \(8 \times 8\) da imagem é decomposto em uma combinação ponderada de 64 padrões de base da DCT — funções cossenoidais de frequência crescente em \(x\) e em \(y\). O coeficiente \(C(u,v)\) representa o “peso” do padrão de frequência \((u,v)\) naquele bloco específico.

A figura abaixo exibe as 64 bases: o canto superior esquerdo corresponde ao componente DC (padrão uniforme); ao avançar para a direita e para baixo, a frequência espacial aumenta progressivamente.

NotaO que é o componente DC?

O coeficiente \(F(0,0)\) — chamado DC (Direct Current) — corresponde à média global de todos os pixels. Na grande maioria das imagens naturais, corresponde ao valor de maior magnitude no espectro — imagens com variações abruptas e periódicas podem concentrar energia em outras frequências. Os demais coeficientes (\(u,v \neq 0,0\)) representam variações em torno da média.

Após fftshift — deslocamento circular que move o DC para o centro —, o espectro fica intuitivo: centro = baixas frequências, bordas = altas frequências.

5.3.5 Implementação e Visualização

Anatomia do Espectro de Fourier 2D (após fftshift)
DC baixas freq. médias freq. altas freq. Espectro de Magnitude |F(u,v)| — escala log Regiões do Espectro DC (0,0) Média global dos pixels Baixas frequências Forma, fundo, iluminação Médias frequências Texturas, padrões Altas frequências Bordas, ruído, detalhes u → freq. horizontal v → freq. vertical Visualização em escala log log(1 + |F|) comprime o intervalo
Figura 5.3: Diagrama conceitual do espectro de Fourier 2D centrado.

5.3.6 O que a Magnitude e a Fase carregam?

Uma das demonstrações mais reveladoras em PDI é trocar os espectros de magnitude e fase entre duas imagens distintas e reconstruir cada uma. O resultado mostra que:

  • A fase carrega a estrutura semântica — contornos, posição dos objetos, geometria da cena;
  • A magnitude carrega o conteúdo energético e textural — contraste, iluminação geral.

Quando reconstruímos uma imagem com a magnitude de A mas a fase de B, o resultado se parece com B — mesmo usando a “energia” de A. A fase possui papel predominante na preservação da estrutura geométrica e organização espacial da imagem, enquanto a magnitude controla a distribuição energética, contraste e textura. Ver um exemplo na Figura 5.4.

Analogia auditiva: no áudio, a fase determina a percepção de posição (esquerda/direita). Na imagem, ela determina a posição e forma dos objetos. Inverter a fase de uma música torna-a irreconhecível mesmo que a magnitude (timbre) permaneça intacta.

# ── Experimento: A Importância da Fase ───────────────────────────────────────
# ── Carregamento da imagem ────────────────────────────────────────────────────
url     = "https://upload.wikimedia.org/wikipedia/commons/2/25/GAZI.MD.AHAD_11.jpg"
caminho = "imagens/coins.jpg"

if not os.path.exists(caminho):
    os.makedirs("imagens", exist_ok=True)
    img_obj = mm.read(url, pil=True)
    mm.write(img_obj, caminho)
else:
    img_obj = mm.read(caminho, pil=True)

img_color = np.array(img_obj)
img_gray  = mm.gray(img_color)

img_a = cv2.resize(img_gray, (400, 400))

# Criar uma imagem B sintética (padrão geométrico)
img_b = np.zeros((400, 400), dtype=np.uint8)
cv2.rectangle(img_b, (100, 100), (300, 300), 255, -1)
cv2.circle(img_b, (200, 200), 150, 128, 10)

FA = np.fft.fft2(img_a)
FB = np.fft.fft2(img_b)

# Troca de Fase
rec_A_mag_B_fase = np.real(np.fft.ifft2(np.abs(FA) * np.exp(1j * np.angle(FB))))
rec_B_mag_A_fase = np.real(np.fft.ifft2(np.abs(FB) * np.exp(1j * np.angle(FA))))

mm.show(
    [img_a, img_b, rec_A_mag_B_fase, rec_B_mag_A_fase],
    titles=["Imagem A", "Imagem B", "Mag(A) + Fase(B)", "Mag(B) + Fase(A)"],
    cols=4, figsize=(16, 4)
)

print("💡 Interpretação: Observe que a estrutura (formas) segue a FASE, não a magnitude.")
Figura 5.4: Experimento de troca de fase: Imagem A (moedas) e Imagem B (padrão geométrico) reconstruídas com magnitudes e fases trocadas. O resultado confirma que a estrutura visual segue a fase — a imagem reconstruída com a fase de B se parece com B, independentemente de qual magnitude foi usada.
💡 Interpretação: Observe que a estrutura (formas) segue a FASE, não a magnitude.

5.3.7 Simulador: Reconstruindo Sinais com Senoides

Antes de prosseguir para imagens 2D, o simulador interativo da Figura 5.5 permite explorar a essência da análise de Fourier 1D: qualquer forma de onda pode ser reconstruída somando-se senoides simples.

Observe como, ao adicionar mais termos, a soma (curva preta) converge progressivamente para a forma alvo (tracejada). O espectro abaixo mostra as amplitudes de cada frequência — é a “receita” da forma de onda.

Explore ativamente: (i) Com quantos termos a onda quadrada fica visualmente aceitável? (ii) Qual das três formas converge mais rapidamente — por quê? (iii) O que acontece com o espectro ao trocar de onda quadrada para triangular?

∿ Simulador: Decomposição de Fourier 1D soma de senoides
Termos
1
Erro RMS
Forma Alvo
quadrada
Forma Alvo
Nº de Termos
1
Exibição
Figura 5.5: Simulador interativo da decomposição de Fourier 1D: visualização da soma de senoides com diferentes frequências, amplitudes e fases. Adicione termos e observe a convergência para formas de onda arbitrárias.

5.4 Teorema da Convolução

O Teorema da Convolução estabelece uma das relações mais importantes entre os domínios espacial e de frequência:

\[ f(x,y) * h(x,y) \;\xleftrightarrow{\mathcal{F}}\; F(u,v) \cdot H(u,v) \tag{5.3}\]

Isso significa que, em vez de deslizar um kernel sobre cada pixel da imagem = convolução (o que é lento para kernels grandes), podemos:

  1. Transformar a imagem para o domínio da frequência via FFT;
  2. Multiplicar ponto a ponto pelo espectro do filtro;
  3. Transformar de volta via IFFT.

5.4.1 Eficiência Computacional: DFT vs Convolução Direta

A implicação prática imediata é de natureza algorítmica. Para um filtro de tamanho \(K \times K\) aplicado a uma imagem \(N \times N\):

Tabela 5.1: Comparação de complexidade computacional entre convolução direta e filtragem via FFT.
Método Complexidade Exemplo (\(N=512\), \(K=51\))
Convolução direta \(O(N^2 K^2)\) \(\approx 682 \times 10^6\) ops
Filtragem via FFT \(O(N^2 \log N)\) \(\approx 2{,}36 \times 10^6\) ops

A filtragem via FFT torna-se significativamente mais eficiente quando o filtro \(h\) é grande. O fluxo computacional é:

\[ g = \mathcal{F}^{-1}\bigl[\mathcal{F}(f) \cdot \mathcal{F}(h)\bigr] \tag{5.4}\]

np.random.seed(42)
h_img, w_img = img_gray.shape
X2, Y2 = np.meshgrid(np.arange(w_img), np.arange(h_img))

# ── 1. Ruído misto ────────────────────────────────────────────────────────────
ruido_gauss  = np.random.normal(0, 15, img_gray.shape)
ruido_period = 30 * np.sin(2 * np.pi * (15 * X2/w_img + 10 * Y2/h_img))
img_noisy    = np.clip(img_gray.astype(float) + ruido_gauss + ruido_period, 0, 255).astype(np.uint8)

# ── 2. Espectro ────────────────────────────────────────────────────────────────
F_n    = np.fft.fftshift(np.fft.fft2(img_noisy.astype(np.float64)))
mag_n  = cv2.normalize(np.log1p(np.abs(F_n)), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)

# ── 3. Filtro Butterworth (remove ruído gaussiano) ────────────────────────────
D_n    = distancia_centro(h_img, w_img)
H_bw   = 1.0 / (1.0 + (D_n / 60.0) ** 6)    # D0=60, n=3

# ── 4. Máscara notch (remove picos periódicos) ────────────────────────────────
mascara_dn = np.ones((h_img, w_img), dtype=np.float64)
for dy, dx in [(+15,+10),(-15,-10),(+15,-10),(-15,+10)]:
    mascara_dn = suprimir_pico(mascara_dn, h_img//2+dy, w_img//2+dx, r=12)

H_total = H_bw * mascara_dn

# ── 5. Filtragem e reconstrução ───────────────────────────────────────────────
img_den = np.real(np.fft.ifft2(np.fft.ifftshift(F_n * H_total)))
img_den = np.clip(img_den, 0, 255).astype(np.uint8)

# ── 6. Métricas ────────────────────────────────────────────────────────────────
psnr_n, ssim_n = cv2.PSNR(img_gray, img_noisy), ssim(img_gray, img_noisy)
psnr_d, ssim_d = cv2.PSNR(img_gray, img_den),   ssim(img_gray, img_den)

print(f"{'Métrica':>10} | {'Imagem ruidosa':>15} | {'Após filtragem':>15}")
print("-"*46)
print(f"{'PSNR (dB)':>10} | {psnr_n:>15.2f} | {psnr_d:>15.2f}")
print(f"{'SSIM':>10} | {ssim_n:>15.4f} | {ssim_d:>15.4f}")

H_vis_dn = cv2.normalize((H_total*255).astype(np.uint8), None, 0, 255, cv2.NORM_MINMAX)

mm.show(
    [img_gray, img_noisy, mag_n, H_vis_dn, img_den],
    titles=["Original",
            f"Ruidosa\nPSNR={psnr_n:.1f} dB",
            "Espectro (picos visíveis)",
            "Filtro Butterworth+Notch",
            f"Restaurada\nPSNR={psnr_d:.1f} dB  SSIM={ssim_d:.3f}"],
    cols=5, figsize=(18, 4)
)
   Métrica |  Imagem ruidosa |  Após filtragem
----------------------------------------------
 PSNR (dB) |           19.97 |           21.21
      SSIM |          0.3478 |          0.6892
Figura 5.6: Pipeline completo: Butterworth + Notch para remoção de ruído misto.
ImportanteO Perigo da Convolução Circular (Wrap-around Error)

A DFT pressupõe que a imagem se repete infinitamente como um azulejo. Se aplicarmos um filtro de frequência sem esticar a imagem com bordas de zeros (zero-padding), o topo da imagem se mistura com a base, e a esquerda com a direita. O padding evita esse vazamento.

# Simulação de um filtro de deslocamento brutal
H_shift = np.zeros_like(img_gray, dtype=complex)
for u in range(M):
    for v in range(N):
        H_shift[u, v] = np.exp(-1j * 2 * np.pi * (u*80/M + v*80/N)) # Desloca 80 pixels

# Filtragem SEM padding (causa o wrap-around)
F_img = np.fft.fft2(img_gray)
img_vazada = np.real(np.fft.ifft2(F_img * H_shift))

img_vazada_vis = cv2.normalize(img_vazada, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
mm.show([img_gray, img_vazada_vis], titles=["Original", "Filtragem s/ Padding (Vazamento)"], cols=2, figsize=(10, 4))
Figura 5.7: Sem padding, um deslocamento severo faz a imagem vazar para o lado oposto (convolução circular).
# ── Kernel Gaussiano 11×11 ────────────────────────────────────────────────────
sigma  = 3.0
K      = 11
ks     = np.arange(K) - K // 2
gauss1d = np.exp(-ks**2 / (2 * sigma**2))
gauss1d /= gauss1d.sum()
kernel  = np.outer(gauss1d, gauss1d)    # kernel 2D separável

# ── Método 1: Convolução espacial direta ─────────────────────────────────────
f_float  = img_gray.astype(np.float64)
conv_esp = cv2.filter2D(f_float, -1, kernel, borderType=cv2.BORDER_CONSTANT)

# ── Método 2: Multiplicação em frequência (via FFT) ──────────────────────────
M, N     = f_float.shape
# Padding para convolução linear (evita aliasing circular)
Mpad     = 2 ** int(np.ceil(np.log2(M + K - 1)))
Npad     = 2 ** int(np.ceil(np.log2(N + K - 1)))

# Posiciona o kernel com a origem no (0,0) e padding com zeros
kernel_pad         = np.zeros((Mpad, Npad))
kh, kw             = kernel.shape
kernel_pad[:kh, :kw] = kernel

F_img   = np.fft.fft2(f_float,  (Mpad, Npad))
F_kern  = np.fft.fft2(kernel_pad)
conv_freq = np.real(np.fft.ifft2(F_img * F_kern))

# Recorte para compensar o deslocamento introduído pelo posicionamento do kernel
offset   = K // 2
conv_freq_crop = conv_freq[offset:offset+M, offset:offset+N]

# ── Verificação numérica ──────────────────────────────────────────────────────
diff = np.abs(conv_esp - conv_freq_crop)
print(f"Diferença máxima  (|conv_esp - conv_freq|): {diff.max():.2e}")
print(f"Diferença média   (|conv_esp - conv_freq|): {diff.mean():.2e}")
print(f"→ Teorema da Convolução verificado numericamente.")

conv_esp_vis  = cv2.normalize(conv_esp,       None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
conv_freq_vis = cv2.normalize(conv_freq_crop, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
diff_vis      = cv2.normalize(diff,           None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)

mm.show(
    [img_gray, conv_esp_vis, conv_freq_vis, diff_vis],
    titles=[
        "Original",
        "Convolução espacial",
        "Multiplicação em frequência",
        f"Diferença (máx={diff.max():.1e})"
    ],
    cols=4, figsize=(16, 5)
)
Diferença máxima  (|conv_esp - conv_freq|): 2.56e-13
Diferença média   (|conv_esp - conv_freq|): 2.79e-14
→ Teorema da Convolução verificado numericamente.
Figura 5.8: Verificação do Teorema da Convolução: a diferença pixel a pixel entre a convolução espacial (cv2.filter2D) e a multiplicação em frequência (FFT) é numericamente nula — confirmando a equivalência teórica.

5.5 Filtros no Domínio da Frequência

Um filtro no domínio da frequência é, essencialmente, uma máscara aplicada ao espectro: valores próximos de 1 deixam a frequência passar; valores próximos de 0 a atenuam. A forma dessa máscara determina o comportamento visual do filtro.

O dilema do corte abrupto: um filtro ideal (corte perfeito em \(D_0\)) parece ótimo na teoria, mas causa o fenômeno de Ringing — oscilações próximas às bordas dos objetos. Isso ocorre porque um corte abrupto no espectro corresponde, no domínio espacial, a uma convolução com uma função sinc de suporte infinito — que produz os anéis visíveis, ver Figura 5.9.

A solução: filtros com transição suave — Gaussiano ou Butterworth — eliminam o ringing ao custo de uma fronteira de corte menos precisa.

# Simulando o Filtro Ideal na Frequência (Cilindro) e sua representação Espacial (Sinc)
N_grid = 128
u = np.arange(-N_grid//2, N_grid//2)
U, V = np.meshgrid(u, u)
D = np.sqrt(U**2 + V**2)

# Frequência: Cilindro Ideal (1 no centro, 0 fora do raio 20)
H_freq = np.zeros((N_grid, N_grid))
H_freq[D <= 20] = 1

# Espaço: A inversa resulta na famigerada Sinc 2D
h_space = np.fft.fftshift(np.real(np.fft.ifft2(np.fft.ifftshift(H_freq))))

fig, ax = plt.subplots(1, 2, subplot_kw={'projection': '3d'}, figsize=(12, 4))
ax[0].plot_surface(U, V, H_freq, cmap='viridis', edgecolor='none')
ax[0].set_title("Frequência: Filtro Ideal (Cilindro)")
ax[0].set_zlim(0, 1.2)

ax[1].plot_surface(U, V, h_space, cmap='plasma', edgecolor='none')
ax[1].set_title("Espaço Real: Ondulações da Sinc (Causa do Ringing)")
plt.tight_layout(); plt.show()
Figura 5.9: A Dualidade Perigosa: O corte abrupto na Frequência (Cilindro) transforma-se obrigatoriamente numa Sinc espacial. Suas ondulações causam o ringing fantasma nas bordas da imagem.

5.5.1 Filtros Passa-Baixa

Filtros passa-baixa atenuam altas frequências, suavizando a imagem e reduzindo ruído. Definem-se em termos da distância ao centro do espectro:

\[ D(u,v) = \sqrt{\left(u - \tfrac{M}{2}\right)^2 + \left(v - \tfrac{N}{2}\right)^2} \tag{5.5}\]

Filtro Ideal (LPFI): \[ H_{\text{ideal}}(u,v) = \begin{cases} 1, & D(u,v) \leq D_0 \\ 0, & D(u,v) > D_0 \end{cases} \tag{5.6}\]

O corte abrupto em \(D_0\) causa o fenômeno de Ringing: oscilações artificiais próximas às bordas dos objetos, análogas ao comportamento da série de Fourier truncada.

Filtro Gaussiano (LPFG): \[ H_{\text{gauss}}(u,v) = e^{-D^2(u,v)/\left(2\sigma^2\right)} \tag{5.7}\]

No domínio contínuo, a transformada de Fourier de uma Gaussiana é também Gaussiana — propriedade que garante transição suave sem ringing.

Filtro Butterworth (LPFB) de ordem \(n\): \[ H_{\text{BW}}(u,v) = \frac{1}{1 + \left[D(u,v)/D_0\right]^{2n}} \tag{5.8}\]

O parâmetro \(n\) controla a inclinação da transição: valores baixos (\(n=1,2\)) produzem transições suaves (sem ringing); valores altos (\(n \geq 5\)) aproximam o comportamento do filtro ideal. Ver exemplos na Figura 5.10.

Perfis dos Filtros Passa-Baixa — comparação visual (D₀ = 30)
D(u,v) H 1.0 0.5 0.0 D₀ Ideal (corte perfeito) → ringing nas bordas Gaussiano → sem ringing Butterworth n=2 Butterworth n=5 zona de transição
À medida que a ordem do Butterworth aumenta, o perfil se aproxima do filtro Ideal — e o ringing aumenta.
Figura 5.10: Filtros passa-baixa.

5.5.2 Filtros Passa-Alta e Passa-Banda

Filtros passa-alta são obtidos pelo complemento de qualquer filtro passa-baixa: \(H_{\text{HP}} = 1 - H_{\text{LP}}\). O efeito visual é o inverso — preserva bordas e detalhes, atenua regiões uniformes.

Filtros passa-banda combinam um corte inferior \(D_L\) e um corte superior \(D_H\), preservando apenas as frequências entre eles: \(H_{\text{BP}} = H_{\text{HP}}^{(D_L)} \cdot H_{\text{LP}}^{(D_H)}\).

São especialmente úteis na remoção de ruído periódico: padrões regulares (interferência elétrica, grade de scanner) aparecem como picos no espectro de magnitude e podem ser suprimidos com um filtro rejeita-banda (notch filter) posicionado sobre esses picos. Ver exemplos de aplicações de filtros passa baixa na Figura 5.12 e passa alta na Figura 5.13.

Frequência de corte D₀ 30 Tipo de filtro
Resposta H(D)
Espectro filtrado |F · H|
Sinal 1D — original vs filtrado
Energia retida por banda (%)
Figura 5.11: Simulador interativo de filtros no domínio da frequência.
import io

def distancia_centro(M, N):
    """Matriz de distâncias ao centro do espectro."""
    u = np.arange(M) - M // 2
    v = np.arange(N) - N // 2
    V, U = np.meshgrid(v, u)
    return np.sqrt(U**2 + V**2)

def aplicar_filtro_freq(img, H):
    """Aplica filtro H (centrado) a imagem via FFT."""
    F  = np.fft.fftshift(np.fft.fft2(img.astype(np.float64)))
    Fg = F * H
    g  = np.real(np.fft.ifft2(np.fft.ifftshift(Fg)))
    return cv2.normalize(g, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)

M, N = img_gray.shape
D    = distancia_centro(M, N)
D0   = 30      # frequência de corte
n_bw = 2       # ordem do Butterworth

# ── Funções de transferência ──────────────────────────────────────────────────
H_ideal = (D <= D0).astype(np.float64)
H_gauss = np.exp(-D**2 / (2 * D0**2))
H_bw    = 1.0 / (1.0 + (D / D0)**(2 * n_bw))

# ── Imagens filtradas ─────────────────────────────────────────────────────────
img_ideal = aplicar_filtro_freq(img_gray, H_ideal)
img_gauss = aplicar_filtro_freq(img_gray, H_gauss)
img_bw    = aplicar_filtro_freq(img_gray, H_bw)

# ── Perfis de H(u,v) ─────────────────────────────────────────────────────────
def fig2img(fig):
    b = io.BytesIO(); fig.savefig(b, format='png', dpi=100); plt.close(fig); b.seek(0)
    return (plt.imread(b)[:,:,:3]*255).astype(np.uint8)

fig, ax = plt.subplots(figsize=(6, 3))
linha   = M // 2
ax.plot(H_ideal[linha, :], label="Ideal",       color="#D85A30", lw=1.5, ls="--")
ax.plot(H_gauss[linha, :], label="Gaussiano",   color="#1D9E75", lw=1.5)
ax.plot(H_bw[linha, :],    label="Butterworth", color="#534AB7", lw=1.5)
ax.axvline(N//2-D0, color="#aaa", lw=0.8, ls=":")
ax.axvline(N//2+D0, color="#aaa", lw=0.8, ls=":")
ax.set(title="Perfis H(u,v) — linha central", xlabel="v", ylabel="H(u,v)")
ax.legend(fontsize=8); plt.tight_layout()
perfil_img = fig2img(fig)

# ── Filtros H visualizados ────────────────────────────────────────────────────
def H_vis(H):
    return cv2.normalize((H*255).astype(np.uint8), None, 0, 255, cv2.NORM_MINMAX)

mm.show(
    [img_gray, img_ideal, img_gauss, img_bw,
     H_vis(H_ideal), H_vis(H_gauss), H_vis(H_bw), perfil_img],
    titles=[
        "Original", "LPF Ideal", "LPF Gaussiano", "LPF Butterworth (n=2)",
        "H Ideal",  "H Gaussiano","H Butterworth", "Perfis H(u,v)"
    ],
    cols=4, figsize=(16, 9)
)
Figura 5.12: Comparação entre filtros passa-baixa: Ideal (D₀=30), Gaussiano (D₀=30) e Butterworth (D₀=30, n=2). Perfis de H(u,v) ao longo de uma linha central e imagens filtradas correspondentes.
# Filtro passa-alta: complemento do passa-baixa Gaussiano
# Reutiliza aplicar_filtro_freq() definida na célula anterior
H_alta   = 1 - H_gauss
img_alta = aplicar_filtro_freq(img_gray, H_alta)

plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1); plt.imshow(img_gray, cmap='gray'); plt.title('Original'); plt.axis('off')
plt.subplot(1, 2, 2); plt.imshow(img_alta, cmap='gray'); plt.title('Passa-alta Gaussiano (D₀=30)'); plt.axis('off')
plt.tight_layout()
plt.show()
Figura 5.13: Filtro passa-alta Gaussiano. (a) Original; (b) Filtro passa-alta (D₀=30) - as bordas das moedas e fundo texturizado são realçados.

5.5.3 Remoção de Ruído Periódico

Ruído periódico — proveniente de interferência elétrica, sensores com padrão regular ou artefatos de digitalização — manifesta-se no espectro de Fourier como picos pontuais simétricos em torno do centro. O filtro rejeita-banda notch suprime seletivamente essas frequências, preservando o restante da imagem.

# ── Imagem com ruído periódico sintético ──────────────────────────────────────
h_img, w_img = img_gray.shape
x = np.arange(w_img); y = np.arange(h_img)
X, Y = np.meshgrid(x, y)

# Ruído senoidal com frequências (u0=20, v0=20) e simétricas
ruido = 40 * np.sin(2 * np.pi * (20 * X / w_img + 20 * Y / h_img))
img_ruidosa = np.clip(img_gray.astype(np.float64) + ruido, 0, 255).astype(np.uint8)

# ── Espectro da imagem ruidosa ───────────────────────────────────────────────
F_r  = np.fft.fftshift(np.fft.fft2(img_ruidosa.astype(np.float64)))
mag_r = np.log1p(np.abs(F_r))
mag_vis = cv2.normalize(mag_r, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)

# ── Máscara notch (suprime os picos do ruído) ────────────────────────────────
mascara = np.ones((h_img, w_img), dtype=np.float64)
r_notch  = 10      # raio da região suprimida

def suprimir_pico(mask, cy, cx, r):
    """Zera um disco de raio r centrado em (cy, cx) na máscara."""
    yy, xx = np.ogrid[:mask.shape[0], :mask.shape[1]]
    dist    = np.sqrt((yy - cy)**2 + (xx - cx)**2)
    mask[dist <= r] = 0
    return mask

# Coordenadas dos picos no espectro centralizado
cy0, cx0 = h_img // 2, w_img // 2
dy0 = int(round(20 * h_img / h_img))   # = 20
dx0 = int(round(20 * w_img / w_img))   # = 20

for dy, dx in [(+dy0,+dx0),(-dy0,-dx0),(+dy0,-dx0),(-dy0,+dx0)]:
    mascara = suprimir_pico(mascara, cy0+dy, cx0+dx, r_notch)

mascara_vis = (mascara * 255).astype(np.uint8)

# ── Filtragem e reconstrução ─────────────────────────────────────────────────
F_filtrada  = F_r * mascara
img_rest     = np.real(np.fft.ifft2(np.fft.ifftshift(F_filtrada)))
img_rest_vis = cv2.normalize(img_rest, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)

psnr = cv2.PSNR(img_gray, img_rest_vis)
print(f"PSNR (original vs restaurada): {psnr:.2f} dB")

mm.show(
    [img_ruidosa, mag_vis, mascara_vis, img_rest_vis],
    titles=[
        "Com ruído periódico",
        "Espectro (log)",
        "Máscara notch",
        f"Restaurada (PSNR={psnr:.1f} dB)"
    ],
    cols=4, figsize=(16, 4)
)
PSNR (original vs restaurada): 30.62 dB
Figura 5.14: Remoção de ruído periódico via filtro notch no domínio da frequência: (a) imagem com ruído senoidal, (b) espectro mostrando os picos do ruído, (c) máscara notch centrada nos picos, (d) imagem restaurada.

📌 Síntese — Filtros Espectrais

Filtro Efeito visual Artefato Uso
Passa-baixa Ideal Suavização forte Ringing (anéis) Ilustrativo apenas
Passa-baixa Gaussiano Suavização suave Nenhum Suavização geral
Passa-baixa Butterworth Suavização controlada Ringing leve (ordens altas) Compromisso suavidade/precisão
Passa-alta Realce de bordas Pode amplificar ruído Detecção de contornos
Notch Remove frequências específicas Pode criar artefatos locais Remoção de ruído periódico

O design de filtros no domínio da frequência é direto e intuitivo — mas os artefatos espaciais (ringing, borramento) emergem de escolhas no espectro.

5.6 Wavelets e Multirresolução

A Transformada de Fourier decompõe o sinal em frequências globais: cada coeficiente \(F(u,v)\) recebe contribuições de toda a imagem, sem informação sobre onde uma frequência ocorre. Uma borda localizada no canto da imagem se mistura ao espectro inteiro — o que limita sua capacidade de representar explicitamente onde determinadas frequências ocorrem espacialmente.

As wavelets (ondaletas) resolvem essa limitação com funções de base compactas — energia concentrada em uma região finita — que podem ser deslocadas e escaladas, oferecendo representação simultânea em frequência e localização espacial.

5.6.1 O Limite de Fourier: Onde ocorreu o evento?

Fourier nos diz perfeitamente quais frequências existem, mas perde totalmente a noção do tempo/espaço de onde elas ocorreram. Veja o experimento abaixo: uma imagem com duas linhas nítidas, e outra com as mesmas linhas deslocadas. O espectro de magnitude é virtualmente idêntico.

img_sinal1 = np.zeros((128, 128)); img_sinal1[:, 20:25] = 1; img_sinal1[100:105, :] = 1
img_sinal2 = np.zeros((128, 128)); img_sinal2[:, 90:95] = 1; img_sinal2[30:35, :] = 1

mag1 = np.log1p(np.abs(np.fft.fftshift(np.fft.fft2(img_sinal1))))
mag2 = np.log1p(np.abs(np.fft.fftshift(np.fft.fft2(img_sinal2))))

mm.show([img_sinal1, mag1, img_sinal2, mag2], titles=["Sinal A", "Espectro A", "Sinal B (Deslocado)", "Espectro B"], cols=4, figsize=(14, 4))
Figura 5.15: Fourier global é cego para a posição. Os espectros não dizem onde as bordas estão.

5.6.2 Transformada Wavelet Discreta 2D

A DWT aplica, separadamente em linhas e colunas, dois filtros complementares — passa-baixa \(h\) (aproximações) e passa-alta \(g\) (detalhes) — seguidos de subamostragem por 2, produzindo quatro subbandas:

\[\text{DWT}(f) = \{\underbrace{\text{LL}}_{\text{aprox.}},\; \underbrace{\text{LH}}_{\text{horiz.}},\; \underbrace{\text{HL}}_{\text{vert.}},\; \underbrace{\text{HH}}_{\text{diag.}}\}\]

Subbanda Filtros aplicados Conteúdo visual
LL baixa × baixa Aproximação suavizada — versão reduzida da imagem
LH baixa × alta Bordas horizontais e variações verticais
HL alta × baixa Bordas verticais e variações horizontais
HH alta × alta Detalhes diagonais, texturas em 45°, cantos

A decomposição é recursiva: aplicando a DWT novamente sobre LL obtém-se o próximo nível. Após \(J\) níveis, a representação hierárquica contém \(3J+1\) subbandas — cada nível com metade da resolução do anterior.

NotaConexão com CNNs

A análise multirresolução das Wavelets possui forte relação conceitual com representações hierárquicas utilizadas em métodos modernos de visão computacional, como as CNNs.

5.6.3 Famílias de Wavelets

Diferentes wavelets resultam em diferentes compromissos entre localização, suavidade e compactação:

Wavelet Suporte Momentos nulos Simetria Uso típico
Haar 2 1 Assimétrica Análise básica, educação
Daubechies db4 8 4 Assimétrica Compressão, análise geral
Symlet sym4 8 4 Quase simétrica Reconstrução de sinais
Biortogonal 5/3 5/3 2/2 Simétrica JPEG 2000 (sem perda)
Biortogonal 9/7 9/7 4/4 Simétrica JPEG 2000 (com perda)

O número de momentos nulos controla a compactação: com \(p\) momentos nulos, a wavelet produz coeficientes exatamente nulos para polinômios de grau até \(p-1\) — quanto maior \(p\), mais coeficientes próximos de zero em regiões suaves e maior a eficiência de compressão.

wavelet_haar = pywt.Wavelet('haar')
wavelet_db4  = pywt.Wavelet('db4')

phi_h, psi_h, x_h = wavelet_haar.wavefun(level=4)
phi_d, psi_d, x_d = wavelet_db4.wavefun(level=4)

fig, ax = plt.subplots(1, 2, figsize=(10, 3))
ax[0].plot(x_h, psi_h, 'b', lw=2); ax[0].set_title("Ondaleta Haar (ψ)")
ax[1].plot(x_d, psi_d, 'g', lw=2); ax[1].set_title("Ondaleta Daubechies 4 (ψ)")
plt.tight_layout(); plt.show()
Figura 5.16: Funções da Wavelet (ψ). Note como elas rapidamente decaem para zero (suporte compacto), ao contrário das senoides infinitas de Fourier.
Decomposição Wavelet 2D — Estrutura Multirresolução (2 níveis)
f(x,y) M × N DWT LL₁ aprox. M/2 × N/2 LH₁ horiz. HL₁ vert. HH₁ diag. Nível 1 — M/2 × N/2 cada DWT sobre LL₁ LL₂ M/4×N/4 LH₂ HL₂ HH₂ Nível 2 Legenda LL — Aproximação LH — Bordas horiz. HL — Bordas vert. HH — Detalhes diag. Cada nível: ½ da resolução anterior
Figura 5.17: Diagrama da decomposição wavelet 2D em dois níveis.
try:
    import pywt
    HAS_PYWT = True
except ImportError:
    import subprocess
    subprocess.run(["pip", "install", "PyWavelets", "-q"])
    import pywt
    HAS_PYWT = True

# ── Decomposição wavelet 2 níveis ─────────────────────────────────────────────
wavelet   = "haar"
img_float = img_gray.astype(np.float64)

# Nível 1
coefs1 = pywt.dwt2(img_float, wavelet)
LL1, (LH1, HL1, HH1) = coefs1

# Nível 2 (aplicado sobre LL1)
coefs2 = pywt.dwt2(LL1, wavelet)
LL2, (LH2, HL2, HH2) = coefs2

def sb_vis(sb):
    """Normaliza subbanda para visualização [0,255]."""
    return cv2.normalize(np.abs(sb), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)

print(f"Forma original     : {img_gray.shape}")
print(f"LL1 (nível 1)      : {LL1.shape}  |  LH1/HL1/HH1: {LH1.shape}")
print(f"LL2 (nível 2)      : {LL2.shape}  |  LH2/HL2/HH2: {LH2.shape}")

imgs_dwt   = [img_gray, sb_vis(LL1), sb_vis(LH1), sb_vis(HL1), sb_vis(HH1),
              sb_vis(LL2), sb_vis(LH2), sb_vis(HL2), sb_vis(HH2)]
titles_dwt = ["Original",
              "LL₁ (aprox.)", "LH₁ (horiz.)", "HL₁ (vert.)", "HH₁ (diag.)",
              "LL₂ (aprox.)", "LH₂ (horiz.)", "HL₂ (vert.)", "HH₂ (diag.)"]

mm.show(imgs_dwt, titles=titles_dwt, cols=5, figsize=(16, 7))
Forma original     : (2560, 1920)
LL1 (nível 1)      : (1280, 960)  |  LH1/HL1/HH1: (1280, 960)
LL2 (nível 2)      : (640, 480)  |  LH2/HL2/HH2: (640, 480)
Figura 5.18: Decomposição wavelet 2D de 2 níveis com wavelet Haar: subbandas LL, LH, HL, HH em cada nível. As subbandas de detalhe revelam estruturas orientadas em diferentes escalas.
wavelets_comp = ["haar", "db4", "sym4", "bior2.2"]
imgs_comp, titles_comp = [], []

for wname in wavelets_comp:
    LL, (LH, HL, HH) = pywt.dwt2(img_float, wname)
    imgs_comp  += [sb_vis(LL), sb_vis(HH)]
    titles_comp += [f"{wname} — LL₁", f"{wname} — HH₁"]

mm.show(imgs_comp, titles=titles_comp, cols=4, figsize=(14, 8))
Figura 5.19: Comparação entre famílias de wavelets: Haar, db4, sym4 e bior2.2. Subbanda LL₁ (aproximação) e HH₁ (diagonal) para cada escolha, ilustrando o compromisso entre compactação e suavidade.
def dwt_threshold_reconstruct(img, wavelet='db4', nivel=2, threshold=0.0):
    """Decompõe, aplica limiar e reconstrói via IDWT."""
    coefs = pywt.wavedec2(img.astype(np.float64), wavelet, level=nivel)
    # Copia e aplica hard thresholding em todos os detalhe
    coefs_t = [coefs[0]]   # LL final não é limiarizado
    for detalhe in coefs[1:]:
        coefs_t.append(tuple(pywt.threshold(sb, threshold, mode='hard') for sb in detalhe))
    rec = pywt.waverec2(coefs_t, wavelet)
    # Recorte para dimensão original
    rec = rec[:img.shape[0], :img.shape[1]]
    return np.clip(rec, 0, 255).astype(np.uint8)

thresholds = [0, 10, 30, 60, 100]
imgs_thr   = [img_gray]
titles_thr = ["Original"]

for t in thresholds:
    rec  = dwt_threshold_reconstruct(img_gray, threshold=t)
    psnr = cv2.PSNR(img_gray, rec)
    imgs_thr  .append(rec)
    titles_thr.append(f"T={t}  PSNR={psnr:.1f} dB")

mm.show(imgs_thr, titles=titles_thr, cols=3, figsize=(14, 10))
Figura 5.20: Reconstrução wavelet com limiamento de coeficientes (hard thresholding): à medida que o limiar aumenta, mais detalhes são zerificados, produzindo imagens progressivamente mais suaves. Métrica PSNR quantifica a perda de qualidade.

5.6.4 📌 Síntese - Fourier vs Wavelets: quando usar cada uma?

Critério Fourier (DFT) Wavelet (DWT)
Base Senoides de suporte infinito Funções compactas, localizadas
Localização espacial ✗ — informação global ✓ — frequência e posição
Filtragem espectral ✓ — controle preciso de banda Limitado
Compressão DCT (JPEG) — excelente em blocos DWT (JPEG 2000) — melhor para imagens inteiras
Análise multi-escala ✓ — hierarquia natural
Ruído periódico ✓ — localização exata no espectro
Texturas não-estacionárias

Regra prática: use Fourier para filtragem espectral e remoção de ruído periódico; use wavelets para compressão, análise multi-escala e processamento de sinais não-estacionários.

5.7 Compressão de Imagens

As wavelets estabelecem a fundação teórica do padrão JPEG 2000, mas o padrão JPEG original — dominante em fotografia digital — utiliza uma transformada mais simples e igualmente eficaz: a Transformada de Cossenos Discreta (DCT). Ambas exploram o mesmo princípio: concentrar a energia da imagem em poucos coeficientes e descartar os demais com impacto visual mínimo.

A compressão visa reduzir a quantidade de dados necessária para armazenar ou transmitir uma imagem, explorando redundâncias presentes na representação original.

5.7.1 Taxonomia das Redundâncias

Três categorias principais de redundância são exploradas por algoritmos de compressão:

Tabela 5.2: Tipos de redundância em imagens e como são exploradas.
Tipo Definição Explorada por
Espacial (interpixel) Pixels vizinhos são altamente correlacionados DCT, codificação preditiva
Espectral (interchannel) Canais de cor são correlacionados (R ≈ G ≈ B em imagens naturais) Conversão YCbCr
Psicovisual O sistema visual humano é insensível a certas variações de alta freq. Quantização JPEG

A compressão é classificada em duas categorias fundamentais:

  • Sem perda (lossless): reconstrução bit-a-bit idêntica ao original. Usada quando integridade é crítica (imagens médicas, documentos).
  • Com perda (lossy): permite distorção controlada para ganhos maiores de compressão. Adequada para fotografia e vídeo, onde o SVH tolera imperfeições.

5.7.2 Transformada de Cossenos Discreta (DCT)

A Transformada de Cossenos Discreta (DCT) é a operação central do padrão JPEG. Diferentemente da DFT (que usa base complexa), a DCT é puramente real, o que a torna computacionalmente mais eficiente em implementações de propósito geral e a DCT é puramente real, o que simplifica implementações computacionais e favorece aplicações de compressão de imagens.

Para um bloco \(f(x,y)\) de dimensões \(N \times N\), a DCT-II 2D é definida como:

\[ C(u,v) = \alpha(u)\,\alpha(v) \sum_{x=0}^{N-1}\sum_{y=0}^{N-1} f(x,y)\, \cos\!\left[\frac{\pi(2x+1)u}{2N}\right] \cos\!\left[\frac{\pi(2y+1)v}{2N}\right] \tag{5.9}\]

onde \(\alpha(0) = \sqrt{1/N}\) e \(\alpha(k) = \sqrt{2/N}\) para \(k > 0\) (fator de normalização ortogonal). O coeficiente \(C(0,0)\) corresponde ao componente DC.

NotaDCT vs DFT: vantagem da compactação de energia

Tanto a DCT quanto a DFT transformam um bloco \(N \times N\) em \(N \times N\) coeficientes. A diferença crítica para compressão é a compactação de energia: para imagens naturais. Para imagens naturais, a DCT frequentemente apresenta melhor compactação de energia nos coeficientes de baixa frequência do que a DFT, favorecendo aplicações de compressão perceptual. Isso ocorre porque a DCT assume simetria par do sinal — equivalente a uma extensão periódica sem descontinuidade — reduzindo o ringing nas bordas do bloco. A consequência prática é que mais coeficientes DCT podem ser zerados sem degradação visível, resultando em maior compressão.

fig, axes = plt.subplots(8, 8, figsize=(6, 6))
fig.subplots_adjust(hspace=0.05, wspace=0.05)
for i in range(8):
    for j in range(8):
        # Criamos um "impulso" isolado na frequência
        coef = np.zeros((8, 8)); coef[i, j] = 1
        # A inversa revela o padrão geométrico espacial equivalente
        b = idct(idct(coef.T, norm='ortho').T, norm='ortho')
        axes[i, j].imshow(b, cmap='gray')
        axes[i, j].axis('off')
plt.suptitle("As 64 Bases da DCT 8x8", y=0.92, fontsize=12, fontweight='bold')
plt.show()
Figura 5.21: O Alfabeto Visual do JPEG: As 64 funções de base da DCT-II. O coeficiente DC fica no topo esquerdo (suave). Ao descer e avançar à direita, a oscilação espacial aumenta drasticamente.

5.7.3 Quantização: a principal fonte de compressão

Após a DCT, cada coeficiente \(C(u,v)\) é dividido por um valor da tabela de quantização \(Q(u,v)\) e arredondado para o inteiro mais próximo. Coeficientes de alta frequência — menos perceptíveis ao SVH — recebem valores altos em \(Q\), forçando o resultado ao arredondamento para zero. Longas sequências de zeros são então comprimidas eficientemente pela codificação entrópica.

Quando o fator de qualidade é baixo, o descarte de altas frequências é agressivo: o bloco \(8 \times 8\) é reconstruído com poucos coeficientes, o que produz os característicos artefatos de bloco visíveis nas fronteiras entre blocos adjacentes.

from scipy.fft import dct, idct

def dct2(bloco):
    """DCT-II 2D ortogonal (separável)."""    
    return dct(dct(bloco.T, norm='ortho').T, norm='ortho')

def idct2(coefs):
    """IDCT-II 2D ortogonal."""    
    return idct(idct(coefs.T, norm='ortho').T, norm='ortho')

# ── Bloco 8×8 centralizado da imagem ─────────────────────────────────────────
cy, cx = img_gray.shape[0]//2, img_gray.shape[1]//2
bloco  = img_gray[cy:cy+8, cx:cx+8].astype(np.float64) - 128.0

C = dct2(bloco)

print("Coeficientes DCT do bloco 8×8:")
print(np.round(C).astype(int))
print(f"\nEnergia DC     : {C[0,0]**2:.1f}")
print(f"Energia total  : {(C**2).sum():.1f}")
print(f"Fração no DC   : {C[0,0]**2 / (C**2).sum():.1%} ← concentração de energia")

# ── Reconstrução progressiva ──────────────────────────────────────────────────
imgs_rec  = [cv2.normalize((bloco+128).astype(np.uint8), None, 0, 255, cv2.NORM_MINMAX)]
titles_rec = ["Bloco original\n(8×8 pixels)"]

for keep in [1, 4, 10, 20, 40, 64]:
    C_trunc = np.zeros_like(C)
    indices = sorted([(u,v) for u in range(8) for v in range(8)], key=lambda p: p[0]+p[1])
    for u, v in indices[:keep]:
        C_trunc[u, v] = C[u, v]
    rec = np.clip(idct2(C_trunc) + 128, 0, 255).astype(np.uint8)
    imgs_rec.append(rec)
    titles_rec.append(f"{keep} coef.\n({keep/64:.0%} do total)")

mm.show(imgs_rec, titles=titles_rec, cols=4, figsize=(12, 7))
Coeficientes DCT do bloco 8×8:
[[192 -48   1   8   0  -1   0   0]
 [-96  54   7 -10   0   0   0   0]
 [ 14 -14   8   0   0   0   0   0]
 [  0   0 -11   1   0   0   0   0]
 [  9 -11   0   0   1   0   1   0]
 [  0   0   0   0   0   0   0   0]
 [  0   0   0   0  -1   0   0   0]
 [  0   0   0   0   0   0   0   0]]

Energia DC     : 36816.0
Energia total  : 52253.0
Fração no DC   : 70.5% ← concentração de energia
Figura 5.22: DCT 2D em bloco 8×8: coeficientes e reconstrução progressiva.

5.7.4 Pipeline JPEG

O padrão JPEG aplica a DCT em blocos disjuntos de \(8 \times 8\) pixels. O pipeline completo envolve seis etapas principais:

\[ \text{RGB} \xrightarrow{\text{(1) YCbCr}} \xrightarrow{\text{(2) Sub-amostragem 4:2:0}} \xrightarrow{\text{(3) Blocos 8×8}} \xrightarrow{\text{(4) DCT}} \xrightarrow{\text{(5) Quantização}} \xrightarrow{\text{(6) Huffman/RLE}} \]

Etapa Operação Fundamento perceptual
1 RGB → YCbCr: separa luminância (Y) de crominância (Cb, Cr) O SVH é ~4× mais sensível a variações de luminância do que de cor
2 Sub-amostragem 4:2:0: Cb e Cr reduzidos à metade da resolução Elimina ~50% dos dados de cor com impacto visual mínimo
3–4 Blocos 8×8 centrados em zero e transformados pela DCT Concentra energia nos primeiros coeficientes
5 Divisão por \(Q(u,v)\) e arredondamento: \(\tilde{C}(u,v) = \text{round}[C(u,v)/Q(u,v)]\) Zeros em altas frequências → principal fonte de compressão
6 Varredura zigzag + RLE + Huffman Comprime longas sequências de zeros resultantes da quantização

A tabela de quantização \(Q\) é o parâmetro central do compromisso qualidade-compressão: o fator de qualidade JPEG (1–100) escala \(Q\) globalmente — valores altos preservam mais coeficientes; valores baixos geram mais zeros e maiores artefatos.

Por que o Ziguezague? A DCT acumula a energia vital no canto superior esquerdo (baixas frequências) e empurra os zeros para a direita e para baixo. Ler a matriz na diagonal (ziguezague) cria uma “corrida de zeros” contínua no final do vetor, permitindo que o algoritmo simplesmente anote “e os próximos 45 elementos são zero”, comprimindo o dado violentamente (Run-Length Encoding).

# ── Tabela de quantização luminância (padrão JPEG) ────────────────────────────
Q_luma = np.array([
    [16,11,10,16,24,40,51,61],
    [12,12,14,19,26,58,60,55],
    [14,13,16,24,40,57,69,56],
    [14,17,22,29,51,87,80,62],
    [18,22,37,56,68,109,103,77],
    [24,35,55,64,81,104,113,92],
    [49,64,78,87,103,121,120,101],
    [72,92,95,98,112,100,103,99]
], dtype=np.float64)

def jpeg_compress_block(bloco, Q_table):
    """DCT → quantização → dequantização → IDCT em bloco 8×8."""
    C  = dct2(bloco.astype(np.float64) - 128)
    Cq = np.round(C / Q_table) * Q_table    # quantiza e dequantiza
    return np.clip(idct2(Cq) + 128, 0, 255)

def jpeg_quality_compress(img, qualidade=50):
    """JPEG simplificado: comprime imagem inteira por blocos 8×8."""
    # Fator de escala: qualidade 50 = sem escala; >50 = maior qualidade
    if qualidade < 50:
        escala = 5000 / qualidade
    else:
        escala = 200 - 2 * qualidade
    Q = np.clip(np.round(Q_luma * escala / 100), 1, 255)
    
    h, w   = img.shape
    result = np.zeros_like(img, dtype=np.float64)
    for r in range(0, h-7, 8):
        for c in range(0, w-7, 8):
            result[r:r+8, c:c+8] = jpeg_compress_block(img[r:r+8, c:c+8], Q)
    return result.astype(np.uint8)

# ── Comparação de fatores de qualidade ───────────────────────────────────────
qualidades = [10, 25, 50, 75, 90]
imgs_jpeg  = [img_gray]
titles_jpeg = ["Original"]

for q in qualidades:
    rec  = jpeg_quality_compress(img_gray, qualidade=q)
    psnr = cv2.PSNR(img_gray, rec)
    imgs_jpeg  .append(rec)
    titles_jpeg.append(f"Q={q}  PSNR={psnr:.1f}dB")

mm.show(imgs_jpeg, titles=titles_jpeg, cols=3, figsize=(14, 10))
Figura 5.23: Pipeline JPEG simplificado: DCT em blocos 8×8, quantização com diferentes fatores de qualidade e reconstrução via IDCT. O artefato de blocos torna-se visível para qualidades baixas (Q=10–20).

5.7.5 Simulador Interativo: Quantização DCT

O simulador abaixo permite explorar o efeito da quantização sobre um bloco \(8 \times 8\) real, visualizando em tempo real:

  • os coeficientes DCT (mapa de calor — quanto maior, mais quente);
  • os coeficientes quantizados (observe os zeros aparecerem com qualidade baixa);
  • o bloco reconstruído e o erro de quantização.
⊞ Simulador: Quantização DCT-JPEG (bloco 8×8) blocos 8×8
Qualidade
50
Coef. ≠ 0
Zeros
Erro MSE
Bloco original (8×8)
Coef. DCT (abs, log)
Coef. quantizados
Bloco reconstruído
50
Figura 5.24: Simulador interativo de compressão DCT-JPEG: ajuste o fator de qualidade e visualize em tempo real os coeficientes zerados, o bloco reconstruído e o erro de quantização.

5.8 Comparação de Formatos de Imagem

A escolha do formato de arquivo impacta diretamente o compromisso entre qualidade, tamanho e velocidade de decodificação. Os três formatos mais relevantes para aplicações web e computação visual são JPEG, PNG e WebP.

5.8.1 Características dos Formatos

Tabela 5.3: Comparação entre os principais formatos de imagem rasterizados.
Característica JPEG PNG WebP
Compressão Com perda Sem perda Com e sem perda
Transparência (alfa)
Suporte a animação Limitado
Algoritmo base DCT + Huffman DEFLATE (LZ77 + Huffman) VP8 / VP8L
Melhor para Fotografia Gráficos, texto, ícones Web (substituto universal)
Pior para Texto, bordas nítidas Fotos de alta resolução Compatibilidade legada

5.8.2 Métricas de Qualidade

Duas métricas objetivas são amplamente usadas para avaliar a qualidade de imagens comprimidas:

PSNR (Peak Signal-to-Noise Ratio): \[ \text{PSNR} = 10\,\log_{10}\!\left(\frac{L^2}{\text{MSE}}\right) \quad [\text{dB}] \tag{5.10}\]

onde \(L = 255\) para imagens de 8 bits e MSE é o erro quadrático médio. Valores típicos: PSNR > 40 dB indica qualidade excelente; 30–40 dB, qualidade boa; < 30 dB, degradação visível.

SSIM (Structural Similarity Index):

\[ \text{SSIM}(f,g) = \frac{(2\mu_f\mu_g + c_1)(2\sigma_{fg} + c_2)}{(\mu_f^2+\mu_g^2+c_1)(\sigma_f^2+\sigma_g^2+c_2)} \tag{5.11}\]

O SSIM combina três fatores medidos em janelas locais: luminância (\(\mu_f, \mu_g\)), contraste (\(\sigma_f, \sigma_g\)) e estrutura (\(\sigma_{fg}\)), ponderados por constantes de estabilidade \(c_1, c_2\). O resultado varia em \([-1, 1]\), com 1 indicando identidade perfeita. Diferentemente do PSNR, o SSIM é sensível à organização espacial dos erros, sendo mais alinhado com a percepção humana — consulte Gonzalez; Woods (2018) para a formulação completa.

NotaPSNR vs SSIM: qual usar?

O PSNR é simples e rápido, mas pode superestimar a qualidade em imagens com artefatos localizados (blocos JPEG) ou subestimá-la quando o ruído é perceptualmente irrelevante (diferença de brilho global). O SSIM captura melhor a percepção humana, mas é mais caro computacionalmente.

Para avaliação rigorosa de algoritmos de compressão, recomenda-se reportar ambas as métricas, além de avaliação subjetiva em estudos perceptuais.

5.8.3 Inspeção Visual: Qualidade não é só PSNR

A filosofia de compressão dita o tipo de degradação. O JPEG corta a imagem em quadrados (DCT), gerando “Mosaicos”. Formatos baseados em wavelets ou filtros preditivos modernos (como WebP/JPEG2000) evitam blocos, mas sofrem de perda de textura e “derretimento” (borramento).

# Recorte de área de alto contraste das imagens já comprimidas no loop anterior (Dando zoom de 4x)
zoom_original = cv2.resize(img_gray[120:200, 150:230], (320, 320), interpolation=cv2.INTER_NEAREST)

rec_jpeg = cv2.imread("imagens/comp_test/coins_q10.jpg", 0)
zoom_jpeg = cv2.resize(rec_jpeg[120:200, 150:230], (320, 320), interpolation=cv2.INTER_NEAREST)

# WebP Q=10
cv2.imwrite("imagens/comp_test/coins_q10.webp", img_gray, [cv2.IMWRITE_WEBP_QUALITY, 10])
rec_webp = cv2.imread("imagens/comp_test/coins_q10.webp", 0)
zoom_webp = cv2.resize(rec_webp[120:200, 150:230], (320, 320), interpolation=cv2.INTER_NEAREST)

mm.show([zoom_original, zoom_jpeg, zoom_webp], titles=["Zoom Original", "JPEG Q=10 (DCT Blocos)", "WebP Q=10 (Suavização)"], cols=3, figsize=(14, 5))
Figura 5.25: Forte compressão (Q=10). À esquerda o artefato de bloco clássico da DCT. À direita, a suavização de bordas característica do WebP.
os.makedirs("imagens/comp_test", exist_ok=True)
resultados = []

# ── JPEG ──────────────────────────────────────────────────────────────────────
for q in [10, 20, 30, 40, 50, 60, 70, 80, 90, 95]:
    path = f"imagens/comp_test/coins_q{q}.jpg"
    cv2.imwrite(path, img_gray, [cv2.IMWRITE_JPEG_QUALITY, q])
    rec  = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    resultados.append({"formato": "JPEG", "qualidade": q,
                       "PSNR": cv2.PSNR(img_gray, rec),
                       "KB":   os.path.getsize(path)/1024})

# ── PNG ───────────────────────────────────────────────────────────────────────
path_png = "imagens/comp_test/coins.png"
cv2.imwrite(path_png, img_gray, [cv2.IMWRITE_PNG_COMPRESSION, 9])
resultados.append({"formato": "PNG", "qualidade": "lossless",
                   "PSNR": float('inf'), "KB": os.path.getsize(path_png)/1024})

# ── WebP ──────────────────────────────────────────────────────────────────────
for q in [50, 75, 90]:
    path_w = f"imagens/comp_test/coins_q{q}.webp"
    cv2.imwrite(path_w, img_gray, [cv2.IMWRITE_WEBP_QUALITY, q])
    rec_w  = cv2.imread(path_w, cv2.IMREAD_GRAYSCALE)
    resultados.append({"formato": "WebP", "qualidade": q,
                       "PSNR": cv2.PSNR(img_gray, rec_w),
                       "KB":   os.path.getsize(path_w)/1024})

# ── Curva taxa-distorção ──────────────────────────────────────────────────────
jpeg_r = [r for r in resultados if r["formato"]=="JPEG"]
webp_r = [r for r in resultados if r["formato"]=="WebP"]
png_r  = [r for r in resultados if r["formato"]=="PNG"]

fig, ax = plt.subplots(figsize=(8, 4.5))
ax.plot([r["KB"] for r in jpeg_r], [r["PSNR"] for r in jpeg_r],
        "o-", label="JPEG", color="#D85A30", lw=2, ms=5)
ax.plot([r["KB"] for r in webp_r], [r["PSNR"] for r in webp_r],
        "s-", label="WebP", color="#534AB7", lw=2, ms=5)
ax.axhline(50, color="#1D9E75", lw=2, ls="--",
           label=f"PNG sem perda ({png_r[0]['KB']:.0f} KB)")
ax.axhspan(40, 60, alpha=0.05, color="#1D9E75", label="Qualidade excelente (PSNR>40)")
ax.set(xlabel="Tamanho do arquivo (KB)", ylabel="PSNR (dB)",
       title="Curva Taxa-Distorção: JPEG × WebP × PNG")
ax.legend(fontsize=9); ax.grid(True, alpha=0.3); plt.tight_layout()
plt.show()

print(f"\nTamanho bruto (sem compressão): {img_gray.nbytes/1024:.0f} KB")
print(f"\n{'Formato':>8} {'Qual.':>6} {'KB':>7} {'PSNR (dB)':>11}")
print("-"*38)
for r in resultados:
    psnr_s = f"{r['PSNR']:>11.2f}" if r['PSNR']!=float('inf') else f"{'∞ (lossless)':>11}"
    print(f"{r['formato']:>8} {str(r['qualidade']):>6} {r['KB']:>7.1f} {psnr_s}")
Figura 5.26: Curva taxa-distorção: PSNR vs tamanho de arquivo para JPEG, WebP e PNG.

Tamanho bruto (sem compressão): 4800 KB

 Formato  Qual.      KB   PSNR (dB)
--------------------------------------
    JPEG     10   103.3       33.50
    JPEG     20   152.0       37.45
    JPEG     30   205.1       39.70
    JPEG     40   217.8       40.96
    JPEG     50   327.7       41.48
    JPEG     60   370.4       45.04
    JPEG     70   398.8       49.71
    JPEG     80   432.4       51.19
    JPEG     90   581.0       54.88
    JPEG     95   775.3       60.12
     PNG lossless  1906.8 ∞ (lossless)
    WebP     50   137.0       39.37
    WebP     75   185.9       41.16
    WebP     90   393.1       45.55
try:
    from skimage.metrics import structural_similarity as ssim
except ImportError:
    import subprocess; subprocess.run(["pip","install","scikit-image","-q"])
    from skimage.metrics import structural_similarity as ssim

qualidades_ssim = [25, 50, 75, 95]
imgs_ssim   = [img_gray]
titles_ssim = ["Original"]

for q in qualidades_ssim:
    path = f"imagens/comp_test/coins_q{q}.jpg"
    rec  = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    if rec is None: continue
    if rec.shape != img_gray.shape:
        rec = cv2.resize(rec, (img_gray.shape[1], img_gray.shape[0]))
    
    psnr_v = cv2.PSNR(img_gray, rec)
    ssim_v, _ = ssim(img_gray, rec, full=True)
    diff_vis = cv2.normalize(np.abs(img_gray.astype(float)-rec.astype(float)),
                             None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
    
    imgs_ssim  += [rec, diff_vis]
    titles_ssim += [f"Q={q}  PSNR={psnr_v:.1f}dB\nSSIM={ssim_v:.3f}",
                    f"Mapa de erro (Q={q})\n(bordas = artefatos de bloco)"]

mm.show(imgs_ssim, titles=titles_ssim, cols=3, figsize=(14, 14))
Figura 5.27: Mapas de erro JPEG em diferentes qualidades: artefatos de bloco nas bordas.

📌 Síntese — Compressão JPEG

Etapa O que faz Ganho
YCbCr Separa luminância de crominância Modela percepção humana
Subamostragem 4:2:0 Reduz Cb/Cr à metade ~50% de dados a menos
DCT 8×8 Concentra energia nos primeiros coeficientes Alta compactação
Quantização Zera coeficientes imperceptíveis Principal fonte de compressão
Huffman Codifica estatisticamente os zeros ~30–50% adicional

Artefatos típicos de JPEG: - Artefato de bloco (qualidades baixas): fronteiras 8×8 visíveis; - Ringing (qualidades muito baixas): oscilações ao redor de bordas nítidas; - Perda de textura (qualidades baixas): regiões com textura fina ficam “plásticas”.

5.9 Aplicação Prática: Remoção de Ruído por Filtragem Espectral

Reunindo as técnicas do capítulo, apresenta-se um pipeline completo de remoção de ruído que combina análise no domínio da frequência com filtragem adaptativa. O objetivo é remover um ruído misto (Gaussiano + periódico) preservando ao máximo a estrutura original da imagem.

\[ \text{Ruidosa} \xrightarrow{\text{FFT}} \xrightarrow{\text{Identif. picos}} \xrightarrow{\text{Filtro Butterworth + Notch}} \xrightarrow{\text{IFFT}} \text{Restaurada} \]

NotaAvaliação: PSNR e SSIM

O par de métricas PSNR / SSIM fornece uma avaliação complementar: - PSNR penaliza uniformemente todos os erros pixel a pixel. - SSIM avalia a preservação de estruturas perceptualmente relevantes.

Um bom filtro maximiza ambos, mas na prática existe um compromisso: filtros muito agressivos reduzem o ruído de alta frequência mas destroem bordas e texturas, o que pode melhorar o PSNR em relação à imagem ruidosa mas reduz o PSNR em relação à imagem original e degrada o SSIM.

5.10 Resumo do Capítulo

A transição do domínio espacial para o domínio da frequência revela a distribuição de energia da imagem, estabelecendo a base matemática para filtragem avançada e compressão de dados. Ver um mapa conceitual do domínio da frequência na Figura 5.28.

%%{init: {
  "theme": "default",
  "flowchart": {
    "useMaxWidth": false
  },
  "themeVariables": {
    "fontSize": "9px"
  }
}}%%

flowchart LR
    A["f(x,y)<br/>Domínio Espacial"] <--> B["DFT / IDFT"]
    B --> C["F(u,v)"]
    D["|F(u,v)|<br/>Magnitude<br/>Energia por frequência"]
    E["φ(u,v)<br/>Fase<br/>Estrutura e posição"]
    
    C --> D
    C --> E
    A -- "Convolução * h" --> F["Multiplicação · H(u,v)"]

    style D fill:#dbeafe,stroke:#2563eb
    style E fill:#fef3c7,stroke:#d97706
Figura 5.28: Mapa conceitual do domínio da frequência.

🔑 Fundamentos Essenciais

  • DFT e Percepção Visual: O espectro decompõe a imagem em ondas. A fase carrega a inteligibilidade da forma e os contornos; a magnitude dita apenas a distribuição de contraste.
  • Eficiência Algorítmica: O Teorema da Convolução permite que filtros de grande escala sejam aplicados via FFT, despencando a complexidade de \(O(N^2 K^2)\) para \(O(N^2 \log N)\).
  • O Artefato do Ringing: Cortes abruptos de frequência (Filtro Ideal) geram ondulações indesejadas no espaço. Filtros Gaussianos e Butterworth garantem transições suaves.
  • A Era das Wavelets: Superando a análise global de Fourier, as Wavelets capturam frequência e localização espacial simultaneamente (multirresolução), fundamentando o JPEG 2000 e a arquitetura das CNNs.
  • Compressão DCT (JPEG): O algoritmo explora a cegueira humana a altas frequências espaciais. A DCT compacta a energia visual em blocos de \(8 \times 8\), quantizando e descartando detalhes finos (o que gera artefatos de bloco em qualidades baixas).

Próximos Passos: O Capítulo 6 inicia a Parte II da obra, estendendo estas ferramentas aos Espaços de Cor e inaugurando os conceitos de Visão Computacional.

5.11 🤖 Uso do NotebookLM como Tutor Complementar

Nesta edição, incentiva-se o uso do NotebookLM como ferramenta complementar de aprendizagem. Baseado em inteligência artificial, o sistema utiliza exclusivamente os documentos fornecidos pelo autor como fonte de conhecimento, produzindo respostas alinhadas ao conteúdo e à abordagem adotada ao longo do livro.

Importante🎓 Estude com o Tutor Inteligente

5.12 Lista de Exercícios

  1. (10%) Implemente a DFT 2D manualmente (sem np.fft.fft2) usando a definição da Equação 5.1 para uma imagem \(16 \times 16\). Compare com np.fft.fft2 e verifique que as diferenças absolutas são menores que \(10^{-8}\). Meça o tempo de execução de ambas as implementações e explique a diferença.

  2. (15%) Adicione ruído senoidal com frequências \((u_0, v_0) \in \{(5,10), (20,5), (30,30)\}\) à imagem de moedas. Para cada frequência, projete um filtro notch e remova o ruído. Avalie quantitativamente a restauração com PSNR e SSIM. Discuta o compromisso entre remoção de ruído e preservação de detalhes.

  3. (15%) Compare os filtros passa-baixa Ideal, Gaussiano e Butterworth (ordens \(n = 1, 2, 4\)) com \(D_0 = 20, 40, 60\) pixels. Para cada combinação, calcule o PSNR e o SSIM da imagem filtrada em relação à original. Apresente os resultados em uma tabela e plote as curvas de resposta em frequência \(H(u, 0)\).

  4. (15%) Implemente a decomposição wavelet 2D manualmente usando a wavelet Haar: calcule os filtros \(h\) (passa-baixa) e \(g\) (passa-alta) de Haar, aplique-os por separabilidade em linhas e colunas, e subamostreie por 2. Compare o resultado com pywt.dwt2(img, 'haar') e verifique a equivalência numérica.

  5. (15%) Aplique o limiamento de coeficientes wavelet (wavelet thresholding) com limiares \(T \in \{5, 10, 20, 40, 80\}\) e wavelets Haar, db4 e sym4. Para cada combinação, calcule PSNR e SSIM após reconstrução com pywt.waverec2. Identifique a combinação wavelet × limiar que maximiza o SSIM.

  6. (15%) Implemente o pipeline JPEG completo incluindo: conversão RGB → YCbCr, subamostragem 4:2:0 de Cb/Cr, DCT em blocos \(8 \times 8\), quantização com tabela padrão escalada por fator de qualidade, e reconstrução com IDCT. Compare os resultados com cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, q]) para qualidades 20, 50 e 80.

  7. (15%) Crie uma imagem sintética composta por três regiões: fotografia de textura natural (canto superior esquerdo), região de texto nítido (centro) e região de gradiente suave (canto inferior direito). Compare JPEG, PNG e WebP para essa imagem mista em termos de PSNR, SSIM e tamanho de arquivo. Justifique qual formato é mais adequado considerando o conteúdo heterogêneo.

Referências do Capítulo

A fundamentação teórica deste capítulo baseia-se nas seguintes obras:

  • Gonzalez; Woods (2018) para os conceitos de DFT 2D, filtros no domínio da frequência, DCT e fundamentos de compressão de imagens.
  • Oppenheim (1999) para a teoria de sinais e sistemas no domínio discreto, incluindo o Teorema da Convolução e propriedades da DFT.
  • Mallat (1999) para a teoria de wavelets, análise multirresolução e bancos de filtros.
  • Wallace (1991) para o padrão de compressão JPEG original e o fundamento da quantização DCT.
  • Szeliski (2022) para métricas de qualidade (PSNR, SSIM) e comparação de formatos de imagem modernos.