5  Transformadas e Compressão

Nos capítulos anteriores, todas as operações foram realizadas no domínio espacial, em que os algoritmos atuam diretamente sobre os valores de intensidade dos pixels.

Neste capítulo será apresentada uma abordagem complementar: o domínio da frequência, no qual a imagem é representada pelas variações espaciais de intensidade, e não apenas pelos valores individuais dos pixels.

O conceito de frequência espacial descreve a rapidez com que a intensidade varia ao longo da imagem. Variações lentas correspondem a baixas frequências, enquanto bordas, detalhes finos e ruídos correspondem a altas frequências.

Essa representação baseia-se no fato de que qualquer imagem digital discreta pode ser decomposta em uma combinação de funções ortogonais. A Transformada de Fourier utiliza uma base de exponenciais complexas bidimensionais (equivalentes a senoides com orientação e frequência específicas). Outras transformadas, como a Transformada de Cossenos (DCT) e a Transformada Wavelet (DWT), utilizam diferentes famílias de funções de base — cossenos bidimensionais no caso da DCT, e funções com suporte compacto no caso das wavelets.

Entre as principais aplicações dessa representação destacam-se:

  1. Filtragem no domínio da frequência, para atenuar ou realçar determinadas faixas de frequência;
  2. Análise multirresolução por meio de transformadas wavelet, que representa estruturas em diferentes escalas;
  3. Compressão de imagens, pela redução do número de coeficientes necessários para representar a imagem.

5.1 Objetivos

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

  • Interpretar o espectro de Fourier de uma imagem, distinguindo magnitude, fase e componentes de frequência;
  • Aplicar o Teorema da Convolução para realizar filtragem no domínio da frequência utilizando a Transformada Rápida de Fourier (FFT);
  • Projetar e analisar filtros no domínio da frequência, compreendendo o funcionamento de filtros passa-baixa, passa-alta e notch;
  • Compreender a análise multirresolução por transformadas wavelet e sua aplicação na representação hierárquica de imagens;
  • Descrever o processo de compressão de imagens, incluindo a Transformada Discreta do Cosseno (DCT) e a quantização dos coeficientes;
  • Selecionar formatos de armazenamento de imagens, como JPEG, PNG e WebP, de acordo com os requisitos da aplicação.

5.2 Configuração do Ambiente

import os, urllib.request

os.makedirs("tmp/state", exist_ok=True)  # artefatos de build da trilha C++

url = "https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph/config.py"
if not os.path.exists("config.py"):
    urllib.request.urlretrieve(url, "config.py")

# Kernel Python mesmo na trilha C++. cpp=True baixa morph.hpp + stb; as
# células %%writefile *.cpp deste capítulo compilam COM OpenCV
# (-DMM_USE_OPENCV + pkg-config opencv4).
import config
config.setup(cpp=True)
from morph import mm
import numpy as np
✅ Ambiente pronto. Morph: 1.1.9 | OpenCV: 5.0.0

5.3 Transformada de Fourier Discreta 2D

A análise de Fourier baseia-se no princípio de que qualquer sinal periódico pode ser representado como uma soma de funções senoidais com diferentes frequências, amplitudes e fases. Esse conceito também se aplica às imagens digitais, permitindo representá-las no domínio da frequência em vez do domínio espacial.

A Figura 5.1 ilustra essa decomposição para um sinal unidimensional. No caso de uma imagem, a Transformada Discreta de Fourier (DFT) converte a matriz de intensidades \(f(x,y)\) em um conjunto de coeficientes que descreve a contribuição das diferentes frequências espaciais presentes na imagem.

%%writefile tmp/fig_decomposicao_1d.cpp
#define MM_OUT "tmp/fig_decomposicao_1d.png"
//| label: fig-decomposicao-1d
//| fig-cap: "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."
//| echo: false
//| output: true

#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <cmath>
#include "morph.hpp"
#include <filesystem>

int main() {
    // Gera pontos x de 0 a 2*pi
    std::vector<double> x(400);
    for (int i = 0; i < 400; ++i) {
        x[i] = 2.0 * M_PI * i / 399.0;
    }

    // Onda quadrada ideal (1.0 para x < pi, -1.0 caso contrário)
    std::vector<double> square(400);
    for (int i = 0; i < 400; ++i) {
        square[i] = (x[i] < M_PI) ? 1.0 : -1.0;
    }

    // Soma das primeiras harmônicas
    std::vector<double> soma(400, 0.0);
    std::vector<std::vector<double>> harms;
    for (int n = 1; n <= 3; ++n) {
        std::vector<double> h(400);
        double coef = (4.0 / M_PI) * (1.0 / (2 * n - 1));
        for (int i = 0; i < 400; ++i) {
            h[i] = coef * std::sin((2 * n - 1) * x[i]);
        }
        harms.push_back(h);
        for (int i = 0; i < 400; ++i) {
            soma[i] += h[i];
        }
    }

    // Prepara as curvas para o gráfico
    std::vector<std::vector<double>> ys = {square, harms[0], harms[1], harms[2], soma};
    std::vector<std::string> labels = {"Onda quadrada ideal", "1a harmonica", "3a harmonica", "5a harmonica", "Soma (3 primeiras)"};
    std::vector<cv::Scalar> colors = {cv::Scalar(40, 40, 40), cv::Scalar(60, 160, 80), cv::Scalar(180, 120, 60), cv::Scalar(150, 80, 160), cv::Scalar(60, 60, 220)};

    // Cria o gráfico de linha (ordem: x único, ys, labels, colors, title, xlabel, ylabel)
    mm::Image chart = mm::lineChart(
        x, ys, labels, colors,
        "Sintese de Fourier: de senos a uma onda quadrada",
        "Posicao", "Intensidade"
    );

    // Exibe resultado
    std::vector<mm::Image> chart_list = {chart};
    mm::show(chart_list, MM_OUT, {"Decomposicao de Fourier 1D"}, 1);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(chart, "tmp/fig_decomposicao_1d_0.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_decomposicao_1d.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_decomposicao_1d.cpp -o tmp/fig_decomposicao_1d -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_decomposicao_1d \
  && test -f "tmp/fig_decomposicao_1d.png" \
  || echo "⚠ mm::show não gravou tmp/fig_decomposicao_1d.png"
[1] Decomposicao de Fourier 1D
try:
    mm.show(
        [
            mm.read("tmp/fig_decomposicao_1d_0.png"),
        ],
        titles=[
            'Decomposicao de Fourier 1D',
        ],
        cols=1,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_decomposicao_1d_0.png (ver a versao Python)")
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.

5.3.1 Simulador: Reconstruindo Sinais com Senoides

Antes de estudar imagens bidimensionais, o simulador da Figura 5.2 ilustra o princípio da análise de Fourier para sinais unidimensionais: uma forma de onda pode ser aproximada pela soma de senoides com diferentes frequências e amplitudes.

À medida que novos termos são adicionados, a soma das senoides (curva preta) aproxima-se da forma de onda de referência (tracejada). O gráfico inferior apresenta o espectro de amplitudes, indicando a contribuição de cada frequência para a reconstrução do sinal.

DicaAtividade

Explore o simulador e responda:

  1. Quantos termos são necessários para obter uma boa aproximação da onda quadrada?
  2. Qual das três formas de onda converge mais rapidamente? Justifique sua resposta.
  3. Como o espectro de amplitudes se altera ao trocar a onda quadrada pela triangular?

1. Quantos termos são necessários para uma boa aproximação da onda quadrada?

Com aproximadamente 15 a 20 termos, a forma da onda já se aproxima bem da referência. Entretanto, próximo às descontinuidades permanece uma pequena oscilação, conhecida como fenômeno de Gibbs, que não desaparece mesmo com a adição de mais termos.

2. Qual forma converge mais rapidamente? Por quê?

A onda triangular converge mais rapidamente, pois as amplitudes de seus harmônicos decaem mais rápido que as da onda quadrada e da onda dente-de-serra. Como consequência, poucos termos já produzem uma boa aproximação.

3. Como o espectro muda entre a onda quadrada e a triangular?

Ambas possuem apenas harmônicos ímpares, mas, na onda triangular, as amplitudes diminuem muito mais rapidamente. Assim, poucos harmônicos são suficientes para reconstruir o sinal com boa precisão.

∿ 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.2: 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.3.2 Interpretação do espectro de frequência

Ao aplicar a Transformada Discreta de Fourier (DFT) a uma imagem e visualizar o módulo de seus coeficientes (ver Figura 5.5), obtém-se o espectro de magnitude, que mostra a distribuição das frequências espaciais presentes na imagem.

O coeficiente localizado na origem da DFT, denominado componente DC (Direct Current), corresponde à frequência nula e representa a intensidade média da imagem. Por convenção, esse coeficiente é armazenado no canto superior esquerdo do espectro. Para facilitar sua interpretação, aplica-se a operação FFT Shift, que desloca a componente DC para o centro da imagem. Após esse deslocamento, as baixas frequências concentram-se na região central, enquanto as altas frequências ficam próximas às bordas, como resume a Tabela 5.1.

Tabela 5.1: Correspondência entre as regiões do espectro de magnitude após a aplicação do FFT Shift.
Região do espectro Componentes predominantes Exemplos na imagem
Centro (baixas frequências) Variações espaciais lentas Iluminação, regiões homogêneas e formas globais
Região intermediária (médias frequências) Variações de escala intermediária Texturas e padrões repetitivos
Bordas (altas frequências) Variações espaciais rápidas Contornos, detalhes finos e ruído

Essa organização facilita a interpretação do espectro e o projeto de filtros. A atenuação das baixas frequências reduz as variações globais de intensidade, enquanto a atenuação das altas frequências suaviza a imagem ao reduzir detalhes finos e parte do ruído.

5.3.3 O Experimento da Grade: Construindo uma Imagem a partir de um Único Coeficiente

Antes de apresentar a formulação matemática da Transformada Discreta de Fourier (DFT), é útil analisar sua inversa, denominada Transformada Discreta Inversa de Fourier (IDFT). Considere um espectro em que todos os coeficientes sejam nulos, exceto um. Um exemplo dessa construção é apresentado no código da Figura 5.3 e pode ser explorado interativamente no simulador da Figura 5.4.

A imagem reconstruída é uma senoide bidimensional. A posição do coeficiente no espectro determina sua orientação e sua frequência espacial, enquanto sua magnitude e sua fase definem, respectivamente, sua amplitude e seu deslocamento espacial. Assim, cada coeficiente da DFT representa uma componente senoidal, e a imagem original pode ser reconstruída pela soma de todas essas componentes.

%%writefile tmp/fig_05_grade_2d.cpp
#define MM_OUT "tmp/fig_05_grade_2d.png"
//| label: fig-05-grade-2d
//| fig-cap: "Toda frequência no espectro (ponto isolado) corresponde a uma onda senoidal 2D rotacionada no domínio espacial."
//| echo: true
//| output: true

#include <opencv2/opencv.hpp>
#include <vector>
#include <complex>
#include <cmath>
#include "morph.hpp"
#include <filesystem>

// Helper para fftshift manual em matriz complexa
void fftshift_complex(cv::Mat& src, cv::Mat& dst) {
    int cx = src.cols / 2;
    int cy = src.rows / 2;
    cv::Mat q0(src, cv::Rect(0, 0, cx, cy));   // Top-Left
    cv::Mat q1(src, cv::Rect(cx, 0, cx, cy));  // Top-Right
    cv::Mat q2(src, cv::Rect(0, cy, cx, cy));  // Bottom-Left
    cv::Mat q3(src, cv::Rect(cx, cy, cx, cy)); // Bottom-Right
    cv::Mat tmp;
    cv::hconcat(q3, q2, tmp);
    cv::hconcat(tmp, q0, tmp);
    cv::hconcat(tmp, q1, dst); // Reorganiza em 4 quadrantes trocados
}

// Helper para calcular magnitude e normalizar
void normalize_float_mat(cv::Mat& src, cv::Mat& dst) {
    cv::normalize(src, dst, 0, 255, cv::NORM_MINMAX, CV_8U);
}

int main() {
    int N_grid = 100;

    // Espectro complexo vazio
    cv::Mat espectro_vazio(N_grid, N_grid, CV_64FC2, cv::Scalar(0, 0));

    // Acendendo um único ponto (frequência) fora do centro
    int u0 = 10, v0 = 5;
    int idx_y = N_grid/2 - v0;
    int idx_x = N_grid/2 - u0;
    espectro_vazio.at<cv::Vec2d>(idx_y, idx_x) = cv::Vec2d(1000, 0);

    // FFT shift manual (para alinhar com o domínio espacial)
    cv::Mat espectro_shifted;
    fftshift_complex(espectro_vazio, espectro_shifted);

    // Retornando para o domínio espacial (IDFT)
    cv::Mat onda_2d_complex;
    cv::idft(espectro_shifted, onda_2d_complex, cv::DFT_SCALE);

    // Extrair parte real
    cv::Mat onda_2d_channels[2];
    cv::split(onda_2d_complex, onda_2d_channels);
    cv::Mat onda_2d = onda_2d_channels[0]; // Parte real

    // Normalizar para visualização
    cv::Mat onda_vis;
    normalize_float_mat(onda_2d, onda_vis);

    // |Especro| para visualização
    cv::Mat magnitude;
    cv::Mat espectro_channels[2];
    cv::split(espectro_vazio, espectro_channels);
    cv::magnitude(espectro_channels[0], espectro_channels[1], magnitude);

    cv::Mat espectro_vis;
    normalize_float_mat(magnitude, espectro_vis);

    // Destaque visual do ponto (converter para BGR e desenhar círculo)
    cv::Mat espectro_color;
    cv::cvtColor(espectro_vis, espectro_color, cv::COLOR_GRAY2BGR);
    cv::circle(espectro_color, 
               cv::Point(N_grid/2 - u0, N_grid/2 - v0), 
               2, cv::Scalar(0, 0, 255), -1);

    // Converter para mm::Image e exibir
    mm::Image img_espectro(espectro_color.rows, espectro_color.cols, espectro_color.channels());
    std::memcpy(img_espectro.data.data(), espectro_color.data, img_espectro.data.size());

    mm::Image img_onda(onda_vis.rows, onda_vis.cols, onda_vis.channels());
    std::memcpy(img_onda.data.data(), onda_vis.data, img_onda.data.size());

    std::vector<mm::Image> imagens = {img_espectro, img_onda};
    std::vector<std::string> titulos = {"Espectro (1 ponto ativo)", "Onda 2D Resultante (IDFT)"};

    mm::show(imagens, MM_OUT, titulos, 2);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(espectro_color, "tmp/fig_05_grade_2d_0.png");
mm::write(onda_vis, "tmp/fig_05_grade_2d_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_grade_2d.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_grade_2d.cpp -o tmp/fig_05_grade_2d -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_grade_2d \
  && test -f "tmp/fig_05_grade_2d.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_grade_2d.png"
[1] Espectro (1 ponto ativo)
[2] Onda 2D Resultante (IDFT)
try:
    mm.show(
        [
            mm.read("tmp/fig_05_grade_2d_0.png"),
            mm.read("tmp/fig_05_grade_2d_1.png"),
        ],
        titles=[
            'Espectro (1 ponto ativo)',
            'Onda 2D Resultante (IDFT)',
        ],
        cols=2,
        figsize=(10, 4),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_grade_2d_0.png (ver a versao Python)")
Figura 5.3: Toda frequência no espectro (ponto isolado) corresponde a uma onda senoidal 2D rotacionada no domínio espacial.
∿ Simulador: Síntese de Frequência 2D (IDFT) Espaço de Fourier
Frequência u
10
Frequência v
5
Distância R
11.18
Ângulo θ
26.6°
Espectro (Clique para mover o ponto)
➔
Onda 2D Resultante (Domínio Espacial)
10
5
Figura 5.4: Simulador interativo da síntese de Fourier 2D. Altere a posição horizontal (\(u\)) e vertical (\(v\)) do coeficiente no espectro de frequências centrado e observe como a distância em relação ao centro dita a frequência espacial (espessura) e o ângulo dita a orientação da onda senoidal gerada.

5.3.4 Definição Matemática

Considere uma imagem \(f(x,y)\) com dimensões \(M \times N\). Sua Transformada Discreta de Fourier 2D (DFT) é definida por:

\[ 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}\]

em que \(u = 0, 1, \ldots, M-1\) e \(v = 0, 1, \ldots, N-1\) representam as frequências discretas nas direções horizontal e vertical, respectivamente. O termo exponencial corresponde a uma senoide bidimensional, cuja frequência e orientação são determinadas pelos índices \((u,v)\).

A Transformada Discreta Inversa de Fourier 2D (IDFT) reconstrói a imagem original a partir de seus coeficientes:

\[ 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}\]

As Equações Equação 5.1 e Equação 5.2 mostram que a DFT e a IDFT formam um par de transformações: a primeira converte a imagem para o domínio da frequência, enquanto a segunda reconstrói exatamente a imagem original a partir de seus coeficientes.

NotaSobre o símbolo \(j\)

O termo \(j\) denota a unidade imaginária, definida por \(j^2 = -1\). Em engenharia e processamento de sinais, adota-se \(j\) em vez de \(i\) para evitar conflito com a notação de corrente elétrica. Sua utilização na exponencial complexa, regida pela fórmula de Euler (\(e^{j\theta} = \cos\theta + j\sin\theta\)), permite representar de forma compacta a amplitude e a fase de cada frequência espacial presente na imagem.

NotaO que é o componente DC?

O coeficiente \(F(0,0)\), denominado componente DC (Direct Current), é igual à soma das intensidades de todos os pixels da imagem (ver Figura 5.5):

\[ F(0,0)=MN\,\bar{f}, \]

em que \(\bar{f}\) é a intensidade média da imagem. Por isso, o componente DC representa o nível médio de intensidade e, na maioria das imagens naturais, possui a maior magnitude do espectro.

Os demais coeficientes representam variações em torno dessa média. Após a aplicação do FFT Shift, o componente DC é deslocado para o centro do espectro, concentrando as baixas frequências na região central e as altas frequências nas bordas.

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.5: Diagrama conceitual do espectro de Fourier 2D centrado.

5.3.5 Magnitude e Fase

Cada coeficiente da Transformada Discreta de Fourier (DFT) é um número complexo e pode ser escrito como

\[ F(u,v)=R(u,v)+j\,I(u,v), \]

em que \(R(u,v)\) e \(I(u,v)\) correspondem, respectivamente, às partes real e imaginária do coeficiente. Da Equação Equação 5.1, obtêm-se

\[ R(u,v)= \sum_{x=0}^{M-1}\sum_{y=0}^{N-1} f(x,y) \cos\!\left( 2\pi\left(\frac{ux}{M}+\frac{vy}{N}\right) \right), \]

e

\[ I(u,v)= - \sum_{x=0}^{M-1}\sum_{y=0}^{N-1} f(x,y) \sin\!\left( 2\pi\left(\frac{ux}{M}+\frac{vy}{N}\right) \right). \]

A partir dessa representação, definem-se duas grandezas fundamentais:

  • Magnitude, que indica a intensidade da componente de frequência,

\[ |F(u,v)|=\sqrt{R(u,v)^2+I(u,v)^2}; \]

  • Fase, que determina o alinhamento (ou deslocamento) espacial da componente,

\[ \phi(u,v)=\operatorname{atan2}\!\left(I(u,v),\,R(u,v)\right). \]

Assim, cada coeficiente também pode ser escrito em sua forma polar,

\[ F(u,v)=|F(u,v)|\,e^{j\phi(u,v)}. \]

O espectro de Fourier pode, portanto, ser visualizado por meio de duas imagens distintas: o espectro de magnitude, normalmente utilizado para analisar a distribuição das frequências, e o espectro de fase, que descreve a organização espacial das componentes senoidais.

Embora o espectro de magnitude seja o mais utilizado para inspeção visual, a fase contém grande parte das informações estruturais da imagem. A combinação de magnitude e fase permite reconstruir exatamente a imagem original por meio da IDFT.

5.3.6 O que a Magnitude e a Fase carregam?

Uma demonstração clássica consiste em combinar a magnitude de uma imagem com a fase de outra e reconstruir o resultado. Esse experimento evidencia que:

  • A fase preserva a estrutura espacial da imagem, incluindo a posição dos objetos, seus contornos e sua geometria. Pequenas alterações na fase podem provocar grandes mudanças visuais.
  • A magnitude controla como a energia é distribuída entre as frequências espaciais, influenciando principalmente o contraste e a textura.

Quando uma imagem é reconstruída com a magnitude de A e a fase de B, o resultado tende a assemelhar-se mais a B do que a A, evidenciando que a fase é o principal componente responsável pela organização espacial da cena. Entretanto, a magnitude continua sendo importante, pois modula o contraste das estruturas reconstruídas. Assim, uma reconstrução fiel depende da combinação consistente entre magnitude e fase.

Um exemplo desse comportamento é apresentado na Figura 5.6.

NotaAnalogia com Áudio: Limitações e Cuidados

A fase de um sinal desempenha papéis distintos em áudio e imagens:

  • Áudio estéreo ou multicanal: a fase relativa entre os canais é fundamental para a percepção da posição das fontes sonoras, por meio das diferenças interaurais de tempo (ITD, Interaural Time Differences).
  • Áudio monaural: a fase absoluta exerce pouca influência perceptual direta.
  • Imagens (DFT): a fase é o principal fator responsável pela organização espacial da cena, enquanto a magnitude modula o contraste e a distribuição da energia entre as frequências.

Em ambos os domínios, a magnitude está relacionada à intensidade das componentes de frequência: em áudio, influencia o timbre e a intensidade percebida; em imagens, influencia o contraste e a textura.

%%writefile tmp/fig_05_dft_intro.cpp
#define MM_OUT "tmp/fig_05_dft_intro.png"
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <cmath>
#include <numeric>
#include "morph.hpp"
#include <filesystem>

//| label: fig-05-dft-intro
//| fig-cap: "Experimento de troca de fase: Imagem A (moedas) e Imagem B (padrão geométrico) reconstruídas com magnitudes e fases trocadas. O resultado mostra que a estrutura visual é **muito mais sensível à fase** do que à magnitude: quando a fase de B é mantida, a imagem resultante preserva a organização espacial de B, mesmo com a magnitude de A. A magnitude, por sua vez, influencia principalmente o contraste e a textura. Observe que a qualidade da reconstrução não é perfeita — há artefatos visíveis —, evidenciando a interdependência entre fase e magnitude para uma representação fiel da imagem."
//| echo: true
//| output: true

// Função auxiliar para centralizar o espectro (fftshift)
void fftShift(cv::Mat& spectrum) {
    // Divide o espectro em 4 quadrantes e troca suas posições
    int cx = spectrum.cols / 2;
    int cy = spectrum.rows / 2;

    // Cria regiões para os 4 quadrantes
    cv::Mat q0(spectrum, cv::Rect(0, 0, cx, cy));   // Topo-esquerda
    cv::Mat q1(spectrum, cv::Rect(cx, 0, cx, cy));  // Topo-direita
    cv::Mat q2(spectrum, cv::Rect(0, cy, cx, cy));  // Baixo-esquerda
    cv::Mat q3(spectrum, cv::Rect(cx, cy, cx, cy)); // Baixo-direita

    // Troca os quadrantes
    cv::Mat tmp;
    q0.copyTo(tmp);
    q3.copyTo(q0);
    tmp.copyTo(q3);

    q1.copyTo(tmp);
    q2.copyTo(q1);
    tmp.copyTo(q2);
}

// Função para conversão cv::Mat para mm::Image
mm::Image cvToMmImage(const cv::Mat& mat) {
    cv::Mat converted;
    if (mat.channels() == 1) {
        // Converte para formato adequado
        cv::Mat normalized;
        cv::normalize(mat, normalized, 0, 255, cv::NORM_MINMAX, CV_8U);
        mm::Image img(normalized.rows, normalized.cols, 1);
        std::memcpy(img.data.data(), normalized.data, img.data.size());
        return img;
    } else {
        cv::Mat bgr;
        cv::cvtColor(mat, bgr, cv::COLOR_BGR2RGB);
        mm::Image img(bgr.rows, bgr.cols, 3);
        std::memcpy(img.data.data(), bgr.data, img.data.size());
        return img;
    }
}

int main() {
    // ── Experimento: A Importância da Fase ───────────────────────────────────────
    // ── Carregamento da imagem ────────────────────────────────────────────────────
    std::string url     = "https://upload.wikimedia.org/wikipedia/commons/2/25/GAZI.MD.AHAD_11.jpg";
    std::string caminho = "imagens/coins.jpg";

    mm::Image img_obj;

    // Verifica se o arquivo existe (simplificado - sempre tenta ler a URL ou usa o caminho)
    // Em C++, verificamos se o arquivo pode ser aberto
    FILE* file = fopen(caminho.c_str(), "rb");
    if (file) {
        fclose(file);
        img_obj = mm::read(caminho);
    } else {
        // Tenta criar diretório e baixar
        std::system("mkdir -p imagens");
        img_obj = mm::read(url);
        mm::write(img_obj, caminho);
    }

    // Converte mm::Image para cv::Mat
    cv::Mat img_color(img_obj.h, img_obj.w, CV_8UC3, img_obj.data.data());

    // Converte para cinza
    cv::Mat img_gray_cv;
    cv::cvtColor(img_color, img_gray_cv, cv::COLOR_RGB2GRAY);

    // Redimensiona para 400x400
    cv::Mat img_a;
    cv::resize(img_gray_cv, img_a, cv::Size(400, 400));

    // Cria uma imagem B sintética (padrão geométrico)
    cv::Mat img_b = cv::Mat::zeros(400, 400, CV_8U);
    cv::rectangle(img_b, cv::Point(100, 100), cv::Point(300, 300), cv::Scalar(255), -1);
    cv::circle(img_b, cv::Point(200, 200), 150, cv::Scalar(128), 10);

    // Converte para float para FFT
    cv::Mat img_a_float, img_b_float;
    img_a.convertTo(img_a_float, CV_32F);
    img_b.convertTo(img_b_float, CV_32F);

    // FFT de A e B
    cv::Mat FA_complex, FB_complex;
    cv::dft(img_a_float, FA_complex, cv::DFT_COMPLEX_OUTPUT);
    cv::dft(img_b_float, FB_complex, cv::DFT_COMPLEX_OUTPUT);

    // Separa canais reais e imaginários
    cv::Mat FA_real[2], FA_imag[2];
    cv::split(FA_complex, FA_real);
    FA_imag[0] = FA_real[0].clone();
    FA_imag[1] = FA_real[1].clone();

    cv::Mat FB_real[2], FB_imag[2];
    cv::split(FB_complex, FB_real);
    FB_imag[0] = FB_real[0].clone();
    FB_imag[1] = FB_real[1].clone();

    // Calcula magnitude e fase de A
    cv::Mat magA, phaseA;
    cv::cartToPolar(FA_real[0], FA_real[1], magA, phaseA);

    // Calcula magnitude e fase de B
    cv::Mat magB, phaseB;
    cv::cartToPolar(FB_real[0], FB_real[1], magB, phaseB);

    // Experimento 1: Magnitude de A, Fase de B
    cv::Mat recA_complex_real, recA_complex_imag;
    cv::polarToCart(magA, phaseB, recA_complex_real, recA_complex_imag);

    cv::Mat recA_complex[2] = {recA_complex_real, recA_complex_imag};
    cv::Mat recA_spectrum;
    cv::merge(recA_complex, 2, recA_spectrum);

    // IFFT para recuperar imagem
    cv::Mat rec_A_mag_B_fase_float;
    cv::idft(recA_spectrum, rec_A_mag_B_fase_float, cv::DFT_SCALE | cv::DFT_REAL_OUTPUT);

    // Normaliza e converte para uint8
    cv::Mat rec_A_mag_B_fase;
    cv::normalize(rec_A_mag_B_fase_float, rec_A_mag_B_fase, 0, 255, cv::NORM_MINMAX, CV_8U);

    // Experimento 2: Magnitude de B, Fase de A
    cv::Mat recB_complex_real, recB_complex_imag;
    cv::polarToCart(magB, phaseA, recB_complex_real, recB_complex_imag);

    cv::Mat recB_complex[2] = {recB_complex_real, recB_complex_imag};
    cv::Mat recB_spectrum;
    cv::merge(recB_complex, 2, recB_spectrum);

    // IFFT para recuperar imagem
    cv::Mat rec_B_mag_A_fase_float;
    cv::idft(recB_spectrum, rec_B_mag_A_fase_float, cv::DFT_SCALE | cv::DFT_REAL_OUTPUT);

    // Normaliza e converte para uint8
    cv::Mat rec_B_mag_A_fase;
    cv::normalize(rec_B_mag_A_fase_float, rec_B_mag_A_fase, 0, 255, cv::NORM_MINMAX, CV_8U);

    // Converte todos para mm::Image para exibição
    mm::Image img_a_mm = cvToMmImage(img_a);
    mm::Image img_b_mm = cvToMmImage(img_b);
    mm::Image rec_A_mm = cvToMmImage(rec_A_mag_B_fase);
    mm::Image rec_B_mm = cvToMmImage(rec_B_mag_A_fase);

    // Exibe as imagens
    std::vector<mm::Image> images = {img_a_mm, img_b_mm, rec_A_mm, rec_B_mm};
    std::vector<std::string> titles = {"Imagem A", "Imagem B", "Mag(A) + Fase(B)", "Mag(B) + Fase(A)"};
    mm::show(images, MM_OUT, titles, 4);

    std::cout << "💡 A fase preserva bordas e contornos; a magnitude controla contraste e" << std::endl;
    std::cout << "textura. Em áudio estéreo, a fase afeta a localização espacial; em" << std::endl;
    std::cout << "imagens, determina a organização da cena." << std::endl;

    // Mantém img_gray (a versão em cinza da imagem original) no escopo
    mm::Image img_gray = cvToMmImage(img_gray_cv);

    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img_gray, "tmp/state/img_gray_20.png");
// [pdi:state-io:end]

// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_a, "tmp/fig_05_dft_intro_0.png");
mm::write(img_b, "tmp/fig_05_dft_intro_1.png");
mm::write(rec_A_mag_B_fase, "tmp/fig_05_dft_intro_2.png");
mm::write(rec_B_mag_A_fase, "tmp/fig_05_dft_intro_3.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_dft_intro.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_dft_intro.cpp -o tmp/fig_05_dft_intro -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_dft_intro \
  && test -f "tmp/fig_05_dft_intro.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_dft_intro.png"
[1] Imagem A
[2] Imagem B
[3] Mag(A) + Fase(B)
[4] Mag(B) + Fase(A)
💡 A fase preserva bordas e contornos; a magnitude controla contraste e
textura. Em áudio estéreo, a fase afeta a localização espacial; em
imagens, determina a organização da cena.
try:
    mm.show(
        [
            mm.read("tmp/fig_05_dft_intro_0.png"),
            mm.read("tmp/fig_05_dft_intro_1.png"),
            mm.read("tmp/fig_05_dft_intro_2.png"),
            mm.read("tmp/fig_05_dft_intro_3.png"),
        ],
        titles=[
            'Imagem A',
            'Imagem B',
            'Mag(A) + Fase(B)',
            'Mag(B) + Fase(A)',
        ],
        cols=4,
        figsize=(16, 4),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_dft_intro_0.png (ver a versao Python)")
Figura 5.6: Experimento de troca de fase: Imagem A (moedas) e Imagem B (padrão geométrico) reconstruídas com magnitudes e fases trocadas. O resultado mostra que a estrutura visual é muito mais sensível à fase do que à magnitude: quando a fase de B é mantida, a imagem resultante preserva a organização espacial de B, mesmo com a magnitude de A. A magnitude, por sua vez, influencia principalmente o contraste e a textura. Observe que a qualidade da reconstrução não é perfeita — há artefatos visíveis —, evidenciando a interdependência entre fase e magnitude para uma representação fiel da imagem.

5.4 Teorema da Convolução e Estratégias de Filtragem

O Teorema da Convolução estabelece uma relação fundamental entre os domínios espacial e da frequência:

\[ f(x,y) \circledast h(x,y) \;\overset{\mathcal{F}}{\longleftrightarrow}\; F(u,v)\,H(u,v) \tag{5.3}\]

em que \(\circledast\) representa a convolução circular discreta. Assim, a convolução entre uma imagem \(f(x,y)\) e um filtro \(h(x,y)\) pode ser substituída pela multiplicação de seus espectros.

Na prática, para obter o mesmo resultado da convolução linear realizada no domínio espacial, aplica-se zero-padding antes da Transformada Rápida de Fourier (FFT), evitando artefatos nas bordas da imagem.

Entretanto, nem sempre a filtragem no domínio da frequência é a alternativa mais eficiente. Para filtros como o Gaussiano e o filtro da média (Box Filter), a propriedade de separabilidade permite reduzir significativamente o custo computacional da convolução no domínio espacial.

5.4.1 Kernel Separável vs. Não Separável

Um kernel separável pode ser escrito como o produto externo de dois vetores unidimensionais,

\[ H = v\,h^T, \]

permitindo que a convolução bidimensional seja substituída por duas convoluções unidimensionais consecutivas: uma na direção horizontal e outra na vertical.

Já um kernel não separável não admite essa decomposição e, portanto, sua convolução deve ser realizada diretamente sobre a vizinhança bidimensional.

Na prática, para um kernel de dimensão \(K \times K\), a convolução direta exige \(K^2\) multiplicações por pixel, enquanto um kernel separável requer apenas \(2K\) multiplicações, reduzindo significativamente o custo computacional.

5.4.2 Análise de Eficiência Computacional

Considere uma imagem de dimensões \(M \times N\) e um filtro quadrado de tamanho \(K \times K\). A Tabela 5.2 compara a complexidade das principais estratégias de filtragem.

Tabela 5.2: Comparação da complexidade da convolução direta, separável e via Transformada Rápida de Fourier (FFT).
Método de Filtragem Complexidade Assintótica Dependência de \(K\) Aplicação típica
Espacial não separável \(\mathcal{O}(MNK^2)\) Quadrática Kernels pequenos e não separáveis
Espacial separável \(\mathcal{O}(MNK)\) Linear Filtros Gaussiano e da média
Via FFT \(\mathcal{O}(MN\log(MN))\) Independente de \(K\) Kernels grandes

Para kernels pequenos, a convolução espacial, especialmente quando o filtro é separável, costuma ser mais eficiente devido ao baixo custo das operações. À medida que o tamanho do kernel aumenta, a filtragem via FFT torna-se mais vantajosa, pois seu custo praticamente independe da dimensão do filtro.

5.4.3 Discussão dos resultados experimentais

O gráfico obtido no ensaio com a imagem das moedas (\(2560 \times 1920\)), apresentado na Figura 5.7, confirma o comportamento previsto pela análise de complexidade computacional.

  1. Convolução não separável (\(\mathcal{O}(MNK^2)\))
    A convolução direta apresenta crescimento quadrático com o tamanho do kernel. Para valores pequenos de \(K\), o custo é baixo, mas aumenta rapidamente à medida que o kernel cresce, tornando-se inviável para aplicações em tempo real.

  2. Filtragem via FFT (\(\mathcal{O}(MN \log(MN))\))
    O custo da FFT depende apenas do tamanho da imagem, sendo independente de \(K\). Por isso, seu desempenho permanece aproximadamente constante ao variar o kernel, tornando-a vantajosa para filtros grandes ou não separáveis.

  3. Convolução separável (\(\mathcal{O}(MNK)\))
    A decomposição do kernel em dois filtros unidimensionais reduz significativamente o custo computacional. Na prática, essa abordagem tende a ser a mais eficiente para filtros separáveis, especialmente em implementações otimizadas.

Em geral, a escolha do método depende do tamanho e da estrutura do kernel. Filtros separáveis são mais eficientes no domínio espacial, enquanto a FFT se torna mais vantajosa para kernels grandes ou múltiplas convoluções no domínio da frequência.

\[ g = \mathcal{F}^{-1}\bigl[\mathcal{F}(f)\cdot \mathcal{F}(h)\bigr] \quad \text{(FFT)} \qquad g = f \circledast h \quad \text{(convolução direta)} \qquad g = (f \circledast v) \circledast h^T \quad \text{(separável)} \tag{5.4}\]

onde:

  • \(f(x,y)\) representa a imagem de entrada;
  • \(h(x,y)\) é o kernel bidimensional do filtro;
  • \(v\) e \(h^T\) são, respectivamente, os vetores vertical e horizontal que compõem o kernel separável.
%%writefile tmp/fig_05_conv_eficiencia.cpp
#define MM_OUT "tmp/fig_05_conv_eficiencia.png"
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <cmath>
#include "morph.hpp"
#include <filesystem>

int main() {
    //| label: fig-05-conv-eficiencia
    //| fig-cap: "Comparação de eficiência: Convolução Não Separável (Espacial 2D), Separável (Espacial 1D) e via FFT."
    //| echo: false
    //| output: true

    // Trilha C++: curvas de CUSTO relativo (contagem de operações) — a forma das
    // curvas (K^2 vs 2K vs log N) e o que importa; a trilha py mede tempos reais.
    std::vector<double> K = {3, 7, 11, 15, 21, 31, 41, 51};
    double N2 = 256.0 * 256.0;

    std::vector<double> t_nao_sep, t_sep;
    for (double k : K) {
        t_nao_sep.push_back(N2 * k * k / 1e6);
        t_sep.push_back(N2 * 2.0 * k / 1e6);
    }

    std::vector<double> t_fft(K.size());
    double logN2 = std::log2(N2);
    for (size_t i = 0; i < K.size(); ++i) {
        t_fft[i] = N2 * logN2 / 1e6;
    }

    // Vetores de vetores para múltiplas curvas (uma por posição)
    std::vector<std::vector<double>> xs = {K, K, K};
    std::vector<std::vector<double>> ys = {t_nao_sep, t_sep, t_fft};

    mm::Image chart = mm::lineChart(
        xs, ys,
        {"Nao Separavel  O(N^2 K^2)", "Separavel  O(N^2 . 2K)", "Via FFT  O(N^2 log N)"},
        {},
        "Custo relativo: Espacial vs Frequencia",
        "Tamanho do kernel  K",
        "Operacoes  (x10^6)"
    );

    std::vector<mm::Image> charts = {chart};
    std::vector<std::string> titles = {"Comparacao de complexidade"};
    mm::show(charts, MM_OUT, titles, 1);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(chart, "tmp/fig_05_conv_eficiencia_0.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_conv_eficiencia.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_conv_eficiencia.cpp -o tmp/fig_05_conv_eficiencia -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_conv_eficiencia \
  && test -f "tmp/fig_05_conv_eficiencia.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_conv_eficiencia.png"
[1] Comparacao de complexidade
try:
    mm.show(
        [
            mm.read("tmp/fig_05_conv_eficiencia_0.png"),
        ],
        titles=[
            'Comparacao de complexidade',
        ],
        cols=1,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_conv_eficiencia_0.png (ver a versao Python)")
Figura 5.7: Comparação de eficiência: Convolução Não Separável (Espacial 2D), Separável (Espacial 1D) e via FFT.
ImportanteO problema da convolução circular (wrap-around)

A Transformada Discreta de Fourier (DFT) assume que a imagem é periodicamente estendida no espaço, isto é, que suas bordas se repetem indefinidamente.

Nessa condição, a multiplicação no domínio da frequência corresponde a uma convolução circular no domínio espacial. Como consequência, regiões opostas da imagem (topo e base, esquerda e direita) passam a interagir artificialmente, conforme ilustrado na Figura 5.8.

A aplicação de zero-padding antes da FFT reduz esse efeito ao estender a imagem com valores nulos nas bordas, aproximando o resultado da convolução linear. Esse comportamento pode ser interpretado à luz do Teorema da Convolução, apresentado na Figura 5.9.

%%writefile tmp/fig_05_padding_error.cpp
#define MM_OUT "tmp/fig_05_padding_error.png"
//| label: fig-05-padding-error
//| fig-cap: "Sem *padding*, um deslocamento severo faz a imagem vazar para o lado oposto (convolução circular)."
//| echo: true
//| output: true

#include <opencv2/opencv.hpp>
#include <complex>
#include <cmath>
#include <vector>
#include <string>
#include "morph.hpp"
#include <filesystem>

// Helper para converter mm::Image para cv::Mat
cv::Mat mmToMat(const mm::Image& img) {
    int type = (img.channels == 1) ? CV_8UC1 : CV_8UC3;
    return cv::Mat(img.h, img.w, type, const_cast<unsigned char*>(img.data.data()));
}

// Helper para fftshift manual
void fftShift(cv::Mat& mat) {
    int cx = mat.cols / 2;
    int cy = mat.rows / 2;
    cv::Mat q0(mat, cv::Rect(0, 0, cx, cy));
    cv::Mat q1(mat, cv::Rect(cx, 0, cx, cy));
    cv::Mat q2(mat, cv::Rect(0, cy, cx, cy));
    cv::Mat q3(mat, cv::Rect(cx, cy, cx, cy));
    cv::Mat tmp;
    q0.copyTo(tmp); q3.copyTo(q0); tmp.copyTo(q3);
    q1.copyTo(tmp); q2.copyTo(q1); tmp.copyTo(q2);
}

// Helper para ifftshift manual
void ifftShift(cv::Mat& mat) {
    fftShift(mat); // ifftshift é o mesmo que fftshift para tamanhos pares/ímpares (inverso)
}

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_20.png");
// [pdi:state-io:end]

    // img_gray já está inicializado (fornecido externamente)

    // ── Definir M e N ─────────────────────────────────────────────────────────────
    int M = img_gray.h;
    int N = img_gray.w;

    // Simulação de um filtro de deslocamento brutal
    cv::Mat img_gray_float;
    mmToMat(img_gray).convertTo(img_gray_float, CV_32F);

    // Criar o filtro H_shift no domínio da frequência
    cv::Mat H_shift_real(M, N, CV_32F), H_shift_imag(M, N, CV_32F);
    for (int u = 0; u < M; u++) {
        for (int v = 0; v < N; v++) {
            double angle = -2.0 * M_PI * (u * 120.0 / M + v * 120.0 / N);
            H_shift_real.at<float>(u, v) = static_cast<float>(cos(angle));
            H_shift_imag.at<float>(u, v) = static_cast<float>(sin(angle));
        }
    }

    // Combinar partes real e imaginária em um único canal complexo
    cv::Mat planes_shift[] = {H_shift_real, H_shift_imag};
    cv::Mat H_shift;
    cv::merge(planes_shift, 2, H_shift);

    // Filtragem SEM padding (causa o wrap-around)
    cv::Mat F_img;
    cv::dft(img_gray_float, F_img, cv::DFT_COMPLEX_OUTPUT);

    // Multiplicar no domínio da frequência
    cv::Mat freq_planes[2];
    cv::split(F_img, freq_planes);
    cv::Mat shift_planes[2];
    cv::split(H_shift, shift_planes);

    cv::Mat result_real, result_imag;
    // (a+bi)*(c+di) = (ac-bd) + (ad+bc)i
    cv::multiply(freq_planes[0], shift_planes[0], result_real); // ac
    cv::Mat temp1; 
    cv::multiply(freq_planes[1], shift_planes[1], temp1); // bd
    cv::subtract(result_real, temp1, result_real);

    cv::multiply(freq_planes[0], shift_planes[1], result_imag); // ad
    cv::Mat temp2;
    cv::multiply(freq_planes[1], shift_planes[0], temp2); // bc
    cv::add(result_imag, temp2, result_imag);

    cv::Mat result_planes[] = {result_real, result_imag};
    cv::Mat filtered_freq;
    cv::merge(result_planes, 2, filtered_freq);

    // Transformada inversa
    cv::Mat img_vazada_float;
    cv::idft(filtered_freq, img_vazada_float, cv::DFT_SCALE | cv::DFT_REAL_OUTPUT);

    // Normalizar para visualização
    cv::Mat img_vazada_vis;
    cv::normalize(img_vazada_float, img_vazada_vis, 0, 255, cv::NORM_MINMAX, CV_8U);

    // Converter de volta para mm::Image para exibição
    mm::Image output_img(img_vazada_vis.rows, img_vazada_vis.cols, 1);
    std::memcpy(output_img.data.data(), img_vazada_vis.data, output_img.data.size());

    // Exibir o resultado
    mm::show({img_gray, output_img}, MM_OUT, 
             {"Original", "Filtragem s/ Padding (Vazamento)"}, 2);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_05_padding_error_0.png");
mm::write(img_vazada_vis, "tmp/fig_05_padding_error_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_padding_error.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_padding_error.cpp -o tmp/fig_05_padding_error -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_padding_error \
  && test -f "tmp/fig_05_padding_error.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_padding_error.png"
[1] Original
[2] Filtragem s/ Padding (Vazamento)
try:
    mm.show(
        [
            mm.read("tmp/fig_05_padding_error_0.png"),
            mm.read("tmp/fig_05_padding_error_1.png"),
        ],
        titles=[
            'Original',
            'Filtragem s/ Padding (Vazamento)',
        ],
        cols=2,
        figsize=(10, 4),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_padding_error_0.png (ver a versao Python)")
Figura 5.8: Sem padding, um deslocamento severo faz a imagem vazar para o lado oposto (convolução circular).
%%writefile tmp/fig_05_conv_teorema.cpp
#define MM_OUT "tmp/fig_05_conv_teorema.png"
//| label: fig-05-conv-teorema
//| fig-cap: "Teorema da Convolução: filtrar no domínio do espaço (Gaussiana) equivale a multiplicar o espectro por $H(u,v)$ na frequência. As saídas coincidem visualmente."
//| echo: true
//| output: true

#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include "morph.hpp"
#include <filesystem>

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_20.png");
// [pdi:state-io:end]

    // Mesma suavização por dois caminhos: espaço vs. frequência.
    int M = img_gray.h;
    int N = img_gray.w;
    cv::Mat H = mm::gaussFilter(M, N, 30);      // H(u,v): transferência passa-baixa gaussiana
    mm::Image f_freq = mm::freqFilter(img_gray, H);    // convolução via multiplicação em frequência
    cv::Mat img_gray_mat = img_gray;
    cv::Mat f_esp_mat;
    cv::GaussianBlur(img_gray_mat, f_esp_mat, cv::Size(31, 31), 5.0);
    mm::Image f_esp = f_esp_mat;

    mm::show(
        std::vector<mm::Image>{img_gray, f_esp, f_freq},
        MM_OUT,
        std::vector<std::string>{"Original", "Espaco: Gaussiana", "Frequencia: H(u,v).F(u,v)"},
        3
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_05_conv_teorema_0.png");
mm::write(f_esp, "tmp/fig_05_conv_teorema_1.png");
mm::write(f_freq, "tmp/fig_05_conv_teorema_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_conv_teorema.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_conv_teorema.cpp -o tmp/fig_05_conv_teorema -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_conv_teorema \
  && test -f "tmp/fig_05_conv_teorema.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_conv_teorema.png"
[1] Original
[2] Espaco: Gaussiana
[3] Frequencia: H(u,v).F(u,v)
try:
    mm.show(
        [
            mm.read("tmp/fig_05_conv_teorema_0.png"),
            mm.read("tmp/fig_05_conv_teorema_1.png"),
            mm.read("tmp/fig_05_conv_teorema_2.png"),
        ],
        titles=[
            'Original',
            'Espaco: Gaussiana',
            'Frequencia: H(u,v).F(u,v)',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_conv_teorema_0.png (ver a versao Python)")
Figura 5.9: Teorema da Convolução: filtrar no domínio do espaço (Gaussiana) equivale a multiplicar o espectro por \(H(u,v)\) na frequência. As saídas coincidem visualmente.
NotaSobre a diferença numérica

A diferença residual da ordem de \(10^{-13}\) não viola o Teorema da Convolução, mas reflete limitações computacionais inerentes à aritmética de ponto flutuante (precisão dupla, ~\(10^{-16}\)) e à ordem de operações entre os dois métodos:

  • Convolução espacial: soma ponderada de vizinhos com arredondamentos sucessivos.
  • Convolução em frequência: envolve três transformadas FFT e uma multiplicação complexa, sujeita a erros de truncamento e quantização.

Portanto, a igualdade teórica é exata, mas a implementação numérica produz uma diferença praticamente nula (erro relativo < \(10^{-12}\)), confirmando o teorema dentro da precisão da máquina.

5.5 Filtros no Domínio da Frequência

Um filtro no domínio da frequência pode ser interpretado como uma função de transferência aplicada ao espectro da imagem. Nessa representação, cada coeficiente de frequência é multiplicado por um valor entre 0 e 1, que determina sua atenuação ou preservação. A forma dessa função define o efeito visual do filtro.

Corte abrupto e ringing. Filtros ideais com transição instantânea em uma frequência de corte \(D_0\) produzem descontinuidades no domínio da frequência. Essa descontinuidade se reflete no domínio espacial como oscilações próximas a bordas, conhecidas como ringing. Esse efeito está associado à convolução com funções de suporte infinito no espaço, como a função sinc, conforme ilustrado na Figura 5.10.

Filtros com transição suave. Alternativas como os filtros Gaussiano e Butterworth suavizam a transição entre regiões de passagem e rejeição, reduzindo o ringing. Em contrapartida, essa suavização implica uma fronteira de separação menos definida entre frequências preservadas e atenuadas.

%%writefile tmp/fig_05_conv_teorema_zoom.cpp
#define MM_OUT "tmp/fig_05_conv_teorema_zoom.png"
//| label: fig-05-conv-teorema-zoom
//| fig-cap: "A Dualidade Perigosa: o corte abrupto na Frequência (cilindro Ideal) vira obrigatoriamente uma *sinc* no espaço. Suas ondulações causam o *ringing* fantasma nas bordas da imagem."
//| echo: true
//| output: true

#include <opencv2/opencv.hpp>
#include "morph.hpp"
#include <vector>
#include <string>
#include <filesystem>

int main() {
    // Filtro Ideal na frequência (cilindro) e sua resposta espacial (sinc 2D).
    int N = 128;
    cv::Mat H_freq = mm::idealFilter(N, N, 20);      // 1 dentro do raio 20, 0 fora
    mm::Image h_space = mm::spatialKernel(H_freq);   // ifft2(H) -> ondulações da sinc (ringing)

    mm::show(
        std::vector<mm::Image>{H_freq, h_space},
        MM_OUT,
        std::vector<std::string>{
            "Frequencia: filtro Ideal (cilindro)",
            "Espaco: ondulacoes da sinc (causa do ringing)"
        },
        2
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(H_freq, "tmp/fig_05_conv_teorema_zoom_0.png");
mm::write(h_space, "tmp/fig_05_conv_teorema_zoom_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_conv_teorema_zoom.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_conv_teorema_zoom.cpp -o tmp/fig_05_conv_teorema_zoom -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_conv_teorema_zoom \
  && test -f "tmp/fig_05_conv_teorema_zoom.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_conv_teorema_zoom.png"
[1] Frequencia: filtro Ideal (cilindro)
[2] Espaco: ondulacoes da sinc (causa do ringing)
try:
    mm.show(
        [
            mm.read("tmp/fig_05_conv_teorema_zoom_0.png"),
            mm.read("tmp/fig_05_conv_teorema_zoom_1.png"),
        ],
        titles=[
            'Frequencia: filtro Ideal (cilindro)',
            'Espaco: ondulacoes da sinc (causa do ringing)',
        ],
        cols=2,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_conv_teorema_zoom_0.png (ver a versao Python)")
Figura 5.10: A Dualidade Perigosa: o corte abrupto na Frequência (cilindro Ideal) vira obrigatoriamente uma sinc no espaço. Suas ondulações causam o ringing fantasma nas bordas da imagem.

5.5.1 Filtros Passa-Baixa

Filtros passa-baixa atenuam componentes de alta frequência, resultando em suavização da imagem e redução de ruído. Após a centralização do espectro (FFT Shift), a distância de cada ponto ao centro é dada por:

\[ 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\) introduz descontinuidades no domínio da frequência, resultando em oscilações no domínio espacial conhecidas como ringing. Esse efeito está associado à convolução com funções de suporte infinito.

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

A suavidade da função Gaussiana no domínio da frequência evita descontinuidades, o que elimina o ringing e produz uma transição gradual entre frequências preservadas e atenuadas.

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 suavidade da transição entre passagem e rejeição de frequências. Valores pequenos produzem transições suaves, enquanto valores grandes aproximam o comportamento do filtro ideal, com maior risco de ringing. Um exemplo comparativo é apresentado na Figura 5.11.

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.11: Filtros passa-baixa.

5.5.2 Filtros Passa-Alta e Passa-Banda

Filtros passa-alta podem ser obtidos a partir de um filtro passa-baixa complementar, definido como:

\[ H_{\text{HP}}(u,v) = 1 - H_{\text{LP}}(u,v) \]

Esse tipo de filtro preserva componentes de alta frequência, realçando bordas e detalhes, enquanto atenua regiões de variação suave.

Filtros passa-banda preservam apenas uma faixa intermediária de frequências, limitada por dois raios \(D_L\) e \(D_H\):

\[ H_{\text{BP}}(u,v) = H_{\text{LP}}^{(D_H)}(u,v)\cdot \left[1 - H_{\text{LP}}^{(D_L)}(u,v)\right] \]

Esse tipo de filtragem é útil quando se deseja remover simultaneamente componentes de baixa e alta frequência, preservando apenas estruturas de escala intermediária.

Uma aplicação importante é a remoção de ruído periódico, no qual padrões regulares aparecem como picos localizados no espectro de magnitude. Esses picos podem ser atenuados por meio de filtros notch (rejeita-banda), posicionados especificamente nas frequências indesejadas.

Exemplos de filtros no domínio da frequência são apresentados no simulador da Figura 5.12, Figura 5.13 e Figura 5.14.

🎛️ Simulador: Filtros no Domínio da Frequência Passa-Baixa / Passa-Alta
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.12: Simulador interativo de filtros no domínio da frequência.
%%writefile tmp/fig_05_filtros_freq.cpp
#define MM_OUT "tmp/fig_05_filtros_freq.png"
// g++ -std=c++17 -O2 snippet.cpp -o snippet $(pkg-config --cflags --libs opencv4)
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <cmath>
#include "morph.hpp"

//| label: fig-05-filtros-freq
//| fig-cap: "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."
//| echo: true
//| output: true

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_20.png");
// [pdi:state-io:end]

    // img_gray is injected here by the cross-cell persistence system

    int M = img_gray.h;
    int N = img_gray.w;
    double D0 = 30;
    int n_bw = 2;

    // ── Funções de transferência (H centrado) via construtores de morph.hpp ───────
    //   Ideal:       1 se D<=D0, senão 0
    //   Gaussiano:   exp(-D^2/(2 D0^2))
    //   Butterworth: 1 / (1 + (D/D0)^(2n))
    cv::Mat H_ideal = mm::idealFilter(M, N, D0);
    cv::Mat H_gauss = mm::gaussFilter(M, N, D0);
    cv::Mat H_bw = mm::butterFilter(M, N, D0, n_bw);

    // ── Imagens filtradas via FFT ────────────────────────────────────────────────
    mm::Image img_ideal = mm::freqFilter(img_gray, H_ideal);
    mm::Image img_gauss = mm::freqFilter(img_gray, H_gauss);
    mm::Image img_bw = mm::freqFilter(img_gray, H_bw);

    // Função para visualizar H como imagem
    auto H_vis = [](const cv::Mat& H) -> mm::Image {
        cv::Mat H_norm;
        cv::normalize(H, H_norm, 0, 255, cv::NORM_MINMAX, CV_8U);
        return mm::Image(H_norm.rows, H_norm.cols, 1);
    };

    // ── Perfis de H(u,v) na linha central, desenhados com cv2.line ───────────────
    int linha = M / 2;
    int Wp = 512, Hp = 288;
    cv::Mat perfil = cv::Mat(Hp, Wp, CV_8UC3, cv::Scalar(255, 255, 255));

    auto traca = [&](const cv::Mat& row, cv::Scalar cor) {
        cv::Point ant;
        bool has_ant = false;
        for (int x = 0; x < N; x++) {
            int px = static_cast<int>(std::round(x * (Wp - 1) / (N - 1)));
            double val = row.at<double>(0, x);
            int py = static_cast<int>(std::round(10 + (1.0 - val) * (Hp - 20)));
            if (has_ant) {
                cv::line(perfil, ant, cv::Point(px, py), cor, 2, cv::LINE_AA);
            }
            ant = cv::Point(px, py);
            has_ant = true;
        }
    };

    cv::Mat row_ideal = H_ideal.row(linha);
    cv::Mat row_gauss = H_gauss.row(linha);
    cv::Mat row_bw = H_bw.row(linha);

    traca(row_ideal, cv::Scalar(216, 90, 48));   // (48, 90, 216) em BGR
    traca(row_gauss, cv::Scalar(29, 158, 117));  // (117, 158, 29) em BGR
    traca(row_bw, cv::Scalar(83, 74, 183));      // (183, 74, 83) em BGR

    // Converte perfil para mm::Image
    mm::Image perfil_img = mm::Image(perfil.rows, perfil.cols, perfil.channels());
    std::memcpy(perfil_img.data.data(), perfil.data, perfil_img.data.size());

    // Cria imagens de visualização para H
    cv::Mat H_ideal_vis, H_gauss_vis, H_bw_vis;
    cv::normalize(H_ideal, H_ideal_vis, 0, 255, cv::NORM_MINMAX, CV_8U);
    cv::normalize(H_gauss, H_gauss_vis, 0, 255, cv::NORM_MINMAX, CV_8U);
    cv::normalize(H_bw, H_bw_vis, 0, 255, cv::NORM_MINMAX, CV_8U);

    mm::Image H_ideal_img = mm::Image(H_ideal_vis.rows, H_ideal_vis.cols, 1);
    std::memcpy(H_ideal_img.data.data(), H_ideal_vis.data, H_ideal_img.data.size());

    mm::Image H_gauss_img = mm::Image(H_gauss_vis.rows, H_gauss_vis.cols, 1);
    std::memcpy(H_gauss_img.data.data(), H_gauss_vis.data, H_gauss_img.data.size());

    mm::Image H_bw_img = mm::Image(H_bw_vis.rows, H_bw_vis.cols, 1);
    std::memcpy(H_bw_img.data.data(), H_bw_vis.data, H_bw_img.data.size());

    std::vector<mm::Image> imagens = {
        img_gray, img_ideal, img_gauss, img_bw,
        H_ideal_img, H_gauss_img, H_bw_img, perfil_img
    };

    std::vector<std::string> titulos = {
        "Original", "LPF Ideal", "LPF Gaussiano", "LPF Butterworth (n=2)",
        "H Ideal", "H Gaussiano", "H Butterworth", "Perfis H(u,v)"
    };

    mm::show(imagens, MM_OUT, titulos, 4);

    return 0;
}
Overwriting tmp/fig_05_filtros_freq.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_filtros_freq.cpp -o tmp/fig_05_filtros_freq -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_filtros_freq \
  && test -f "tmp/fig_05_filtros_freq.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_filtros_freq.png"
[1] Original
[2] LPF Ideal
[3] LPF Gaussiano
[4] LPF Butterworth (n=2)
[5] H Ideal
[6] H Gaussiano
[7] H Butterworth
[8] Perfis H(u,v)
try:
    mm.show(mm.read("tmp/fig_05_filtros_freq.png"), figsize=(16, 9))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_filtros_freq.png (ver a versao Python)")
Figura 5.13: 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.
%%writefile tmp/fig_05_filtros_passa_alta.cpp
#define MM_OUT "tmp/fig_05_filtros_passa_alta.png"
//| label: fig-05-filtros-passa-alta
//| fig-cap: "Filtro passa-alta Gaussiano. (a) Original; (b) Filtro passa-alta (D₀=30) - as bordas das moedas e fundo texturizado são realçados."

#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include "morph.hpp"
#include <filesystem>

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_20.png");
// [pdi:state-io:end]

    // Filtro passa-alta = complemento do passa-baixa Gaussiano (1 - H_gauss),
    // construído com mm.gaussFilter(..., highpass=true) e aplicado via FFT.
    int M = img_gray.h;
    int N = img_gray.w;
    double D0 = 30;
    cv::Mat H_alta = mm::gaussFilter(M, N, D0, true);
    mm::Image img_alta = mm::freqFilter(img_gray, H_alta);

    mm::show(
        std::vector<mm::Image>{img_gray, img_alta},
        MM_OUT,
        {"Original", "Passa-alta Gaussiano ($D_0=30$)"},
        2
    );
    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_05_filtros_passa_alta_0.png");
mm::write(img_alta, "tmp/fig_05_filtros_passa_alta_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_filtros_passa_alta.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_filtros_passa_alta.cpp -o tmp/fig_05_filtros_passa_alta -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_filtros_passa_alta \
  && test -f "tmp/fig_05_filtros_passa_alta.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_filtros_passa_alta.png"
[1] Original
[2] Passa-alta Gaussiano ($D_0=30$)
try:
    mm.show(
        [
            mm.read("tmp/fig_05_filtros_passa_alta_0.png"),
            mm.read("tmp/fig_05_filtros_passa_alta_1.png"),
        ],
        titles=[
            'Original',
            'Passa-alta Gaussiano ($D_0=30$)',
        ],
        cols=2,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_filtros_passa_alta_0.png (ver a versao Python)")
Figura 5.14: 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 — associado a interferências elétricas, padrões regulares de sensores ou artefatos de digitalização — aparece no espectro de Fourier como picos pontuais simétricos em torno do centro.

O filtro rejeita-banda (notch) atenua seletivamente essas frequências, preservando as demais componentes da imagem. Um exemplo de aplicação é apresentado na Figura 5.15.

%%writefile tmp/fig_05_ruido_periodico.cpp
#define MM_OUT "tmp/fig_05_ruido_periodico.png"
// Compile: g++ -std=c++17 -O2 $(pkg-config --cflags --libs opencv4) -o snippet snippet.cpp
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>
#include <cmath>
#include <vector>
#include <string>
#include <cstdio>
#include "morph.hpp"

//| label: fig-05-ruido-periodico
//| fig-cap: "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."
//| echo: true
//| output: true

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_20.png");
// [pdi:state-io:end]

    // Ruído senoidal 2D -> espectro -> máscara notch nos 4 picos simétricos -> restauração.
    int h_img = img_gray.h;
    int w_img = img_gray.w;

    // Criar gradientes X e Y (coordenadas de malha)
    cv::Mat X(h_img, w_img, CV_32F);
    cv::Mat Y(h_img, w_img, CV_32F);
    for (int j = 0; j < w_img; j++) {
        for (int i = 0; i < h_img; i++) {
            X.at<float>(i, j) = j;
            Y.at<float>(i, j) = i;
        }
    }

    int u0 = 20, v0 = 20;  // frequências exatas do ruído

    // Construir ruído senoidal 2D
    cv::Mat ruido(h_img, w_img, CV_64F);
    for (int i = 0; i < h_img; i++) {
        for (int j = 0; j < w_img; j++) {
            double arg = 2.0 * M_PI * (u0 * j / (double)w_img + v0 * i / (double)h_img);
            ruido.at<double>(i, j) = 40.0 * std::sin(arg);
        }
    }

    // Converter imagem para float64 e adicionar ruído
    cv::Mat img_gray_d = cv::Mat(h_img, w_img, CV_64F);
    cv::Mat img_gray_8uc1 = cv::Mat(h_img, w_img, CV_8UC1, img_gray.data.data());
    img_gray_8uc1.convertTo(img_gray_d, CV_64F);

    cv::Mat img_ruidosa_d = img_gray_d + ruido;
    cv::Mat img_ruidosa_8u;
    cv::normalize(img_ruidosa_d, img_ruidosa_8u, 0, 255, cv::NORM_MINMAX, CV_8U);
    mm::Image img_ruidosa(h_img, w_img, 1);
    std::memcpy(img_ruidosa.data.data(), img_ruidosa_8u.data, img_ruidosa.data.size());

    // Espectro de magnitude (log) da imagem ruidosa
    mm::Image mag_vis = mm::spectrumMag(img_ruidosa);

    // Máscara notch: 1.0 em todo o plano, disco de 0.0 em cada um dos 4 picos
    cv::Mat mascara = cv::Mat::ones(h_img, w_img, CV_64F);
    int r_notch = 8;
    int cy = h_img / 2, cx = w_img / 2;

    // Coordenadas de malha
    cv::Mat yy(h_img, w_img, CV_64F);
    cv::Mat xx(h_img, w_img, CV_64F);
    for (int i = 0; i < h_img; i++) {
        for (int j = 0; j < w_img; j++) {
            yy.at<double>(i, j) = i;
            xx.at<double>(i, j) = j;
        }
    }

    // Pares de picos simétricos
    std::vector<std::pair<int, int>> picos = {{v0, u0}, {-v0, -u0}, {v0, -u0}, {-v0, u0}};
    for (const auto& pico : picos) {
        int dy = pico.first;
        int dx = pico.second;
        int center_y = cy + dy;
        int center_x = cx + dx;
        for (int i = 0; i < h_img; i++) {
            for (int j = 0; j < w_img; j++) {
                double dist_sq = (yy.at<double>(i, j) - center_y) * (yy.at<double>(i, j) - center_y) +
                                 (xx.at<double>(i, j) - center_x) * (xx.at<double>(i, j) - center_x);
                double dist = std::sqrt(dist_sq);
                if (dist <= r_notch) {
                    mascara.at<double>(i, j) = 0.0;
                }
            }
        }
    }
    cv::Mat mascara_8u;
    mascara.convertTo(mascara_8u, CV_8U, 255.0);
    mm::Image mascara_vis(h_img, w_img, 1);
    std::memcpy(mascara_vis.data.data(), mascara_8u.data, mascara_vis.data.size());

    // Filtragem: aplica a máscara centrada como filtro no domínio da frequência
    mm::Image img_rest_vis = mm::freqFilter(img_ruidosa, mascara);

    double psnr = cv::PSNR(img_gray_8uc1, (cv::Mat)img_rest_vis);
    char buf[256];
    std::snprintf(buf, sizeof(buf), "PSNR (original vs restaurada): %.2f dB", psnr);
    std::string psnr_str(buf);

    // Exibir imagens
    std::vector<mm::Image> imagens = {img_ruidosa, mag_vis, mascara_vis, img_rest_vis};
    std::vector<std::string> titles = {
        "Com ruído periódico",
        "Espectro (log)",
        "Máscara notch",
        "Restaurada (PSNR=" + [psnr] { char b[32]; std::snprintf(b, 32, "%.1f dB", psnr); return std::string(b); }() + ")"
    };
    mm::show(imagens, MM_OUT, titles, 4);

    return 0;
}
Overwriting tmp/fig_05_ruido_periodico.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_ruido_periodico.cpp -o tmp/fig_05_ruido_periodico -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_ruido_periodico \
  && test -f "tmp/fig_05_ruido_periodico.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_ruido_periodico.png"
[1] Com ruído periódico
[2] Espectro (log)
[3] Máscara notch
[4] Restaurada (PSNR=33.2 dB)
try:
    mm.show(mm.read("tmp/fig_05_ruido_periodico.png"), figsize=(16, 4))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_ruido_periodico.png (ver a versao Python)")
Figura 5.15: 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 intensa Ringing Ilustrativo
Passa-baixa Gaussiano Suavização suave Não apresenta ringing Suavização geral
Passa-baixa Butterworth Suavização controlada Ringing (ordens altas) Compromisso entre suavização e seletividade
Passa-alta Realce de bordas Amplificação de ruído Detecção de contornos
Notch Remoção seletiva de frequências Possíveis distorções locais Remoção de ruído periódico

O projeto de filtros no domínio da frequência consiste na definição de máscaras espectrais. Entretanto, efeitos no domínio espacial, como ringing e borramento, emergem diretamente dessas 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 explícita sobre a localização espacial dessas frequências. Assim, estruturas localizadas, como bordas, são representadas de forma distribuída no espectro.

As wavelets (ondaletas) superam essa limitação ao utilizar funções base localizadas no espaço, que podem ser deslocadas e escaladas. Essas funções possuem suporte compacto, isto é, são diferentes de zero apenas em uma região finita do domínio, permitindo uma representação simultânea em termos de frequência e localização espacial.

5.6.1 O Limite da Transformada de Fourier: localização espacial

A Transformada de Fourier descreve com precisão quais frequências estão presentes em um sinal, mas não representa explicitamente onde essas frequências ocorrem no espaço.

No experimento apresentado na Figura 5.16, duas imagens com estruturas localizadas em posições diferentes produzem espectros de magnitude praticamente idênticos. Isso ocorre porque a representação de Fourier é global: cada coeficiente recebe contribuição de toda a imagem.

Como consequência, o espectro de magnitude não representa explicitamente a localização de bordas ou outras estruturas, apenas a distribuição das frequências presentes. Essa limitação motivou o desenvolvimento de representações multirresolução, como a Transformada Wavelet Discreta (DWT), capazes de descrever simultaneamente a frequência e a localização espacial das estruturas da imagem.

%%writefile tmp/fig_05_fracasso_fourier.cpp
#define MM_OUT "tmp/fig_05_fracasso_fourier.png"
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <cmath>
#include "morph.hpp"
#include <filesystem>

//| label: fig-05-fracasso-fourier
//| fig-cap: "Fourier global é cego para a posição. Os espectros não dizem onde as bordas estão."
//| echo: true
//| output: true

int main() {
    // Cria os sinais de teste
    cv::Mat img_sinal1 = cv::Mat::zeros(128, 128, CV_32F);
    img_sinal1(cv::Rect(0, 20, 128, 5)).setTo(1.0f);
    img_sinal1(cv::Rect(100, 0, 5, 128)).setTo(1.0f);

    cv::Mat img_sinal2 = cv::Mat::zeros(128, 128, CV_32F);
    img_sinal2(cv::Rect(0, 90, 128, 5)).setTo(1.0f);
    img_sinal2(cv::Rect(30, 0, 5, 128)).setTo(1.0f);

    // Calcula os espectros de Fourier (transformada, shift e magnitude logarítmica)
    auto compute_log_magnitude = [](cv::Mat& src) -> cv::Mat {
        // FFT 2D
        cv::Mat padded;
        int m = cv::getOptimalDFTSize(src.rows);
        int n = cv::getOptimalDFTSize(src.cols);
        cv::copyMakeBorder(src, padded, 0, m - src.rows, 0, n - src.cols, cv::BORDER_CONSTANT, cv::Scalar::all(0));

        cv::Mat planes[] = {padded, cv::Mat::zeros(padded.size(), CV_32F)};
        cv::Mat complexI;
        cv::merge(planes, 2, complexI);
        cv::dft(complexI, complexI);

        // Shift para o centro
        int cx = complexI.cols / 2;
        int cy = complexI.rows / 2;
        cv::Mat q0(complexI, cv::Rect(0, 0, cx, cy));
        cv::Mat q1(complexI, cv::Rect(cx, 0, complexI.cols - cx, cy));
        cv::Mat q2(complexI, cv::Rect(0, cy, cx, complexI.rows - cy));
        cv::Mat q3(complexI, cv::Rect(cx, cy, complexI.cols - cx, complexI.rows - cy));
        cv::Mat tmp;
        q0.copyTo(tmp);
        q3.copyTo(q0);
        tmp.copyTo(q3);
        q1.copyTo(tmp);
        q2.copyTo(q1);
        tmp.copyTo(q2);

        // Magnitude e log
        cv::Mat planes_shift[2];
        cv::split(complexI, planes_shift);
        cv::Mat mag;
        cv::magnitude(planes_shift[0], planes_shift[1], mag);
        mag += cv::Scalar::all(1.0f);
        cv::log(mag, mag);
        return mag;
    };

    cv::Mat mag1 = compute_log_magnitude(img_sinal1);
    cv::Mat mag2 = compute_log_magnitude(img_sinal2);

    // Normaliza para exibição
    cv::Mat img1_8u, img2_8u, mag1_8u, mag2_8u;
    cv::normalize(img_sinal1, img1_8u, 0, 255, cv::NORM_MINMAX, CV_8U);
    cv::normalize(img_sinal2, img2_8u, 0, 255, cv::NORM_MINMAX, CV_8U);
    cv::normalize(mag1, mag1_8u, 0, 255, cv::NORM_MINMAX, CV_8U);
    cv::normalize(mag2, mag2_8u, 0, 255, cv::NORM_MINMAX, CV_8U);

    // Converte para mm::Image e exibe
    std::vector<mm::Image> images;
    images.push_back(mm::Image(img1_8u.cols, img1_8u.rows, 1));
    std::memcpy(images[0].data.data(), img1_8u.data, images[0].data.size());

    images.push_back(mm::Image(mag1_8u.cols, mag1_8u.rows, 1));
    std::memcpy(images[1].data.data(), mag1_8u.data, images[1].data.size());

    images.push_back(mm::Image(img2_8u.cols, img2_8u.rows, 1));
    std::memcpy(images[2].data.data(), img2_8u.data, images[2].data.size());

    images.push_back(mm::Image(mag2_8u.cols, mag2_8u.rows, 1));
    std::memcpy(images[3].data.data(), mag2_8u.data, images[3].data.size());

    std::vector<std::string> titles = {"Sinal A", "Espectro A", "Sinal B (Deslocado)", "Espectro B"};
    mm::show(images, MM_OUT, titles, 4);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_sinal1, "tmp/fig_05_fracasso_fourier_0.png");
mm::write(mag1, "tmp/fig_05_fracasso_fourier_1.png");
mm::write(img_sinal2, "tmp/fig_05_fracasso_fourier_2.png");
mm::write(mag2, "tmp/fig_05_fracasso_fourier_3.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_fracasso_fourier.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_fracasso_fourier.cpp -o tmp/fig_05_fracasso_fourier -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_fracasso_fourier \
  && test -f "tmp/fig_05_fracasso_fourier.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_fracasso_fourier.png"
[1] Sinal A
[2] Espectro A
[3] Sinal B (Deslocado)
[4] Espectro B
try:
    mm.show(
        [
            mm.read("tmp/fig_05_fracasso_fourier_0.png"),
            mm.read("tmp/fig_05_fracasso_fourier_1.png"),
            mm.read("tmp/fig_05_fracasso_fourier_2.png"),
            mm.read("tmp/fig_05_fracasso_fourier_3.png"),
        ],
        titles=[
            'Sinal A',
            'Espectro A',
            'Sinal B (Deslocado)',
            'Espectro B',
        ],
        cols=4,
        figsize=(14, 4),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_fracasso_fourier_0.png (ver a versao Python)")
Figura 5.16: 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 Transformada Wavelet Discreta (DWT) aplica, separadamente nas direções horizontal e vertical, dois filtros complementares: um passa-baixa \(h\) (aproximação) e um passa-alta \(g\) (detalhes), seguidos de subamostragem por um fator de 2 em cada dimensão. Esse processo produz quatro subbandas, cujos nomes indicam a combinação dos filtros aplicados em cada direção (L = Low-pass, passa-baixa; H = High-pass, passa-alta). As características de cada subbanda são resumidas na Tabela 5.3.

\[ \text{DWT}(f)=\{\underbrace{\text{LL}}_{\text{aprox.}},\; \underbrace{\text{LH}}_{\text{detalhes horizontais}},\; \underbrace{\text{HL}}_{\text{detalhes verticais}},\; \underbrace{\text{HH}}_{\text{detalhes diagonais}}\}. \]

Tabela 5.3: Subbandas produzidas pela Transformada Wavelet Discreta 2D (DWT), indicando os filtros aplicados em cada direção e o conteúdo predominante de cada componente.
Subbanda Filtros aplicados Conteúdo visual
LL baixa × baixa Aproximação da imagem (versão suavizada e reduzida)
LH baixa × alta Bordas horizontais e variações verticais
HL alta × baixa Bordas verticais e variações horizontais
HH alta × alta Detalhes diagonais e texturas

A decomposição pode ser aplicada recursivamente sobre a subbanda LL, gerando uma representação multirresolução. Após \(J\) níveis, obtém-se uma estrutura com \(3J+1\) subbandas, em que cada novo nível reduz a resolução da componente de aproximação.

NotaConexão com CNNs

A decomposição multirresolução das wavelets possui uma relação conceitual com as representações hierárquicas utilizadas em redes neurais convolucionais (CNNs). Em ambos os casos, sucessivas etapas de filtragem e redução de resolução produzem descrições cada vez mais abstratas da imagem. Entretanto, as wavelets utilizam filtros matematicamente definidos e reconstruíveis, enquanto as CNNs aprendem seus filtros durante o treinamento.

5.6.3 Famílias de Wavelets

Diferentes famílias de wavelets apresentam compromissos distintos entre suporte espacial, suavidade e capacidade de compressão. O suporte corresponde à extensão da função wavelet no domínio espacial: quanto menor o suporte, mais localizada é a função; quanto maior, mais suave tende a ser sua representação, porém com maior custo computacional. A Tabela 5.4 compara algumas das famílias mais utilizadas.

Tabela 5.4: Comparação entre famílias de wavelets, destacando o comprimento do suporte, o número de momentos nulos, a simetria e aplicações típicas.
Wavelet Comprimento do suporte Momentos nulos Simetria Uso típico
Haar 2 1 Assimétrica Introdução e análise básica
Daubechies db4 8 4 Assimétrica Compressão e 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

Os momentos nulos medem a capacidade da wavelet de representar regiões suaves da imagem com poucos coeficientes diferentes de zero. Uma wavelet com \(p\) momentos nulos anula exatamente polinômios de grau até \(p-1\). Em consequência, quanto maior o número de momentos nulos, maior tende a ser a eficiência de compressão em regiões homogêneas, embora isso geralmente implique funções com suporte mais longo.

A Figura 5.17 apresenta as funções de base (wavelets) \(\psi(t)\) no domínio espacial. Essas funções possuem suporte compacto, isto é, são diferentes de zero apenas em uma região finita do domínio, ao contrário das senoides da Transformada de Fourier, que se estendem por todo o domínio.

%%writefile tmp/fig_05_wavelet_functions.cpp
#define MM_OUT "tmp/fig_05_wavelet_functions.png"
//| label: fig-05-wavelet-functions
//| fig-cap: "Funções da *Wavelet* (ψ). Note como elas rapidamente decaem para zero (suporte compacto), ao contrário das senoides infinitas de Fourier."
//| echo: true
//| output: true

#include <vector>
#include <string>
#include "morph.hpp"
#include <filesystem>

int main() {
    // psi via mm.wavefun (algoritmo em cascata) — suporte compacto: decai a zero.
    std::vector<double> x_h, phi_h, psi_h;
    mm::wavefun("haar", 6, x_h, phi_h, psi_h);
    std::vector<double> x_d, phi_d, psi_d;
    mm::wavefun("db4", 6, x_d, phi_d, psi_d);

    mm::Image chart_h = mm::lineChart(x_h, std::vector<std::vector<double>>{psi_h}, {}, {(180, 60, 40)}, 
                                      "Ondaleta Haar (psi)", "t");
    mm::Image chart_d = mm::lineChart(x_d, std::vector<std::vector<double>>{psi_d}, {}, {(60, 140, 40)}, 
                                      "Ondaleta Daubechies 4 (psi)", "t");

    mm::show({chart_h, chart_d}, MM_OUT, {"Haar", "Daubechies 4"}, 2);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(chart_h, "tmp/fig_05_wavelet_functions_0.png");
mm::write(chart_d, "tmp/fig_05_wavelet_functions_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_wavelet_functions.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_wavelet_functions.cpp -o tmp/fig_05_wavelet_functions -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_wavelet_functions \
  && test -f "tmp/fig_05_wavelet_functions.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_wavelet_functions.png"
[1] Haar
[2] Daubechies 4
try:
    mm.show(
        [
            mm.read("tmp/fig_05_wavelet_functions_0.png"),
            mm.read("tmp/fig_05_wavelet_functions_1.png"),
        ],
        titles=[
            'Haar',
            'Daubechies 4',
        ],
        cols=2,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_wavelet_functions_0.png (ver a versao Python)")
Figura 5.17: Funções da Wavelet (ψ). Note como elas rapidamente decaem para zero (suporte compacto), ao contrário das senoides infinitas de Fourier.

O diagrama da Figura 5.18 ilustra a análise multirresolução realizada pela DWT, na qual a subbanda de aproximação (LL) é sucessivamente decomposta, formando uma representação hierárquica com dois níveis.

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.18: Diagrama da decomposição wavelet 2D em dois níveis.

O simulador da Figura 5.19 permite explorar interativamente a Transformada Wavelet Discreta 2D (DWT) utilizando a wavelet de Haar. A decomposição em subbandas evidencia a separação entre a componente de aproximação e as componentes de detalhe da imagem.

Os diferentes padrões de entrada permitem observar o comportamento direcional dos filtros. Em imagens com bordas horizontais e verticais, as subbandas LH e HL destacam, respectivamente, as variações verticais e horizontais da intensidade. Em regiões de variação suave, a maior parte da energia concentra-se na subbanda de aproximação LL, enquanto as subbandas de detalhe apresentam coeficientes próximos de zero.

A análise multirresolução também pode ser observada ao aumentar o número de níveis de decomposição. Nesse caso, apenas a subbanda \(\text{LL}_1\) é novamente decomposta, originando as subbandas \(\text{LL}_2\), \(\text{LH}_2\), \(\text{HL}_2\) e \(\text{HH}_2\), que formam o segundo nível da representação hierárquica.

Em padrões formados por regiões homogêneas de grande extensão, como um degradê suave ou um tabuleiro composto por blocos grandes, a energia permanece predominantemente concentrada na subbanda LL. No degradê, isso ocorre porque as diferenças entre pixels vizinhos são pequenas. No tabuleiro, por sua vez, os pixels possuem praticamente a mesma intensidade no interior de cada bloco, de modo que apenas as fronteiras entre blocos produzem coeficientes não nulos nas subbandas de detalhe. Como essas fronteiras ocupam apenas uma pequena fração da imagem, sua contribuição para a energia total permanece reduzida.

Para viabilizar a análise visual dessas variações sutis, o simulador incorpora um controle de ganho de contraste dos detalhes (variando de 1 a 8). Esse parâmetro funciona como um fator de amplificação linear aplicado exclusivamente aos coeficientes das subbandas de detalhe (LH, HL e HH) antes de sua renderização em tela. Em cenários de transição suave (como o gradiente) ou de uniformidade local (como o interior dos blocos do tabuleiro), as diferenças numéricas calculadas pelo filtro passa-altas de Haar resultam em coeficientes muito próximos de zero, o que tornaria os quadrantes correspondentes escuros e imperceptíveis a olho nu. Ao multiplicar esses valores pelo ganho, o simulador resgata visualmente as estruturas de alta frequência ocultas e realça a orientação das bordas remanescentes.

O gráfico de energia por subbanda quantifica essa distribuição entre a componente de aproximação e as componentes de detalhe, demonstrando que o ganho visual não altera a métrica original da energia. Em imagens naturais, a maior parte da energia concentra-se na subbanda LL, enquanto as subbandas LH, HL e HH representam principalmente bordas, texturas e outras variações locais da intensidade.

🌊 Simulador: Decomposição Wavelet 2D Transformada Haar
Imagem Original
Decomposição Wavelet (Mosaico)
Energia por Subbanda (%) — Soma Preservada (Parseval)
LL — Aproximação
Versão suavizada e reduzida da imagem
LH — Detalhe Horizontal
Realça bordas horizontais (variação vertical)
HL — Detalhe Vertical
Realça bordas verticais (variação horizontal)
HH — Detalhe Diagonal
Texturas e cantos (variação em ambas direções)
Figura 5.19: Simulação da decomposição wavelet 2D.

5.6.4 Análise Multirresolução com a DWT 2D

A Transformada Wavelet Discreta 2D (DWT) decompõe uma imagem em componentes de aproximação e detalhe, organizadas de forma hierárquica em diferentes escalas e orientações. Como as subbandas de detalhe em imagens naturais frequentemente apresentam coeficientes de baixo contraste, os exemplos práticos a seguir utilizam um padrão geométrico sintético gerado em Python. Essa abordagem replica o comportamento do simulador da Figura 5.19, tornando visualmente explícitos os efeitos da filtragem espacial e da decomposição multirresolução.

5.6.4.1 Decomposição em Mosaico de Múltiplos Níveis

A Figura 5.20 ilustra a estrutura hierárquica da DWT em dois níveis utilizando a wavelet de Haar. O processo baseia-se na aplicação combinada de filtros passa-baixa e passa-alta nas direções horizontal e vertical, seguidos por subamostragem por um fator de 2.

No primeiro nível, a imagem original origina a subbanda de aproximação (\(LL_1\)) e as componentes de detalhe horizontal (\(LH_1\)), vertical (\(HL_1\)) e diagonal (\(HH_1\)). Na análise multirresolução, a subbanda \(LL_1\) é novamente filtrada e subamostrada, gerando o segundo nível de decomposição (\(LL_2\), \(LH_2\), \(HL_2\) e \(HH_2\)).

Para viabilizar a interpretação visual das componentes de detalhe, o código extrai o valor absoluto de seus coeficientes e aplica uma normalização linear (min-max) para ocupar toda a faixa dinâmica de tons de cinza [0, 255]. Essa operação transforma regiões homogêneas (coeficientes nulos) em preto e destaca em branco as bordas e texturas extraídas em cada escala e orientação.

%%writefile tmp/fig_05_dwt_subbandas.cpp
#define MM_OUT "tmp/fig_05_dwt_subbandas.png"
//| label: fig-05-dwt-subbandas
//| fig-cap: "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 utilizando um padrão sintético."
//| echo: true
//| output: true

#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <cmath>
#include <iostream>
#include "morph.hpp"

// ── Geração da Imagem Sintética (Mesmo padrão 'combined' do simulador) ────────
cv::Mat gerar_imagem_sintetica(int N = 256) {
    cv::Mat img(N, N, CV_64F);
    for (int y = 0; y < N; y++) {
        for (int x = 0; x < N; x++) {
            double v = 55 + 35 * ((double)x / N) + 15 * std::sin(y / 24.0);
            // Quadrado
            if (24 < x && x < 100 && 24 < y && y < 100) {
                v = 225;
            }
            // Círculo
            int cx = 190, cy = 76, r = 34;
            if ((x - cx) * (x - cx) + (y - cy) * (y - cy) < r * r) {
                v = 205;
            }
            // Textura periódica (inferior)
            if (y > 164 && y < 244) {
                int p = 12;
                v = (((x / p + y / p) % 2) == 0) ? 185 : 65;
            }
            // Linha diagonal
            if (std::abs(x - y) < 4) {
                v = 240;
            }
            v = std::max(0.0, std::min(255.0, v));
            img.at<double>(y, x) = v;
        }
    }
    cv::Mat img8;
    img.convertTo(img8, CV_8U);
    return img8;
}

// Normaliza subbanda para visualização [0,255]
cv::Mat sb_vis(const cv::Mat& sb) {
    cv::Mat sb_abs, sb_norm;
    cv::absdiff(sb, cv::Scalar(0), sb_abs);
    cv::normalize(sb_abs, sb_norm, 0, 255, cv::NORM_MINMAX);
    cv::Mat sb_u8;
    sb_norm.convertTo(sb_u8, CV_8U);
    return sb_u8;
}

int main() {
    // ── Decomposição wavelet 2 níveis ─────────────────────────────────────────────
    std::string wavelet = "haar";

    // Substitui a imagem escura de moedas pelo padrão sintético claro
    cv::Mat img_gray = gerar_imagem_sintetica(256);

    cv::Mat img_float;
    img_gray.convertTo(img_float, CV_64F);

    // Nível 1
    mm::Subbands coefs1 = mm::dwt2(img_float, wavelet);
    cv::Mat LL1 = coefs1.LL;
    cv::Mat LH1 = coefs1.LH;
    cv::Mat HL1 = coefs1.HL;
    cv::Mat HH1 = coefs1.HH;

    // Nível 2 (aplicado sobre LL1)
    mm::Subbands coefs2 = mm::dwt2(LL1, wavelet);
    cv::Mat LL2 = coefs2.LL;
    cv::Mat LH2 = coefs2.LH;
    cv::Mat HL2 = coefs2.HL;
    cv::Mat HH2 = coefs2.HH;

    std::cout << "Forma original     : " << img_gray.rows << "x" << img_gray.cols << std::endl;
    std::cout << "LL1 (nível 1)      : " << LL1.rows << "x" << LL1.cols << "  |  LH1/HL1/HH1: " << LH1.rows << "x" << LH1.cols << std::endl;
    std::cout << "LL2 (nível 2)      : " << LL2.rows << "x" << LL2.cols << "    |  LH2/HL2/HH2: " << LH2.rows << "x" << LH2.cols << std::endl;

    std::vector<mm::Image> imgs_dwt = {
        mm::Image(img_gray), 
        mm::Image(sb_vis(LL1)), mm::Image(sb_vis(LH1)), mm::Image(sb_vis(HL1)), mm::Image(sb_vis(HH1)),
        mm::Image(sb_vis(LL2)), mm::Image(sb_vis(LH2)), mm::Image(sb_vis(HL2)), mm::Image(sb_vis(HH2))
    };

    std::vector<std::string> titles_dwt = {
        "Original",
        "LL1 (aprox.)", "LH1 (horiz.)", "HL1 (vert.)", "HH1 (diag.)",
        "LL2 (aprox.)", "LH2 (horiz.)", "HL2 (vert.)", "HH2 (diag.)"
    };

    mm::show(imgs_dwt, MM_OUT, titles_dwt, 5);

    return 0;
}
Overwriting tmp/fig_05_dwt_subbandas.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_dwt_subbandas.cpp -o tmp/fig_05_dwt_subbandas -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_dwt_subbandas \
  && test -f "tmp/fig_05_dwt_subbandas.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_dwt_subbandas.png"
Forma original     : 256x256
LL1 (nível 1)      : 128x128  |  LH1/HL1/HH1: 128x128
LL2 (nível 2)      : 64x64    |  LH2/HL2/HH2: 64x64
[1] Original
[2] LL1 (aprox.)
[3] LH1 (horiz.)
[4] HL1 (vert.)
[5] HH1 (diag.)
[6] LL2 (aprox.)
[7] LH2 (horiz.)
[8] HL2 (vert.)
[9] HH2 (diag.)
try:
    mm.show(mm.read("tmp/fig_05_dwt_subbandas.png"), figsize=(16, 7))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_dwt_subbandas.png (ver a versao Python)")
Figura 5.20: 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 utilizando um padrão sintético.

5.6.4.2 O Compromisso entre Localização e Suavidade

A escolha da função de base (wavelet) influencia diretamente a forma como as feições da imagem são distribuídas e codificadas pelos coeficientes da DWT. A Figura 5.21 compara os resultados práticos obtidos ao aplicar quatro famílias distintas sobre o padrão geométrico sintético: haar, db4, sym4 e bior2.2.

Por possuir suporte curto e formato de função degrau, a wavelet de Haar produz coeficientes altamente localizados nas descontinuidades espaciais, gerando bordas finas e nítidas nas subbandas de detalhe. Em contrapartida, famílias como Daubechies (db4) e Symlets (sym4), que apresentam maior suporte (filtros mais longos) e maior número de momentos nulos, geram respostas mais suaves e distribuídas ao redor das transições, o que pode introduzir leves oscilações ou borramentos nas fronteiras abruptas.

Esse comportamento evidencia o clássico compromisso (trade-off) da análise de multirresolução: suportes menores favorecem a localização espacial exata das bordas, enquanto suportes maiores e maior número de momentos nulos tendem a produzir representações mais esparsas e suaves. Essa suavidade e capacidade de atenuação de altas frequências garantem maior eficiência na compactação da energia, características fundamentais para aplicações de compressão de dados e remoção de ruído (denoising).

%%writefile tmp/fig_05_dwt_wavelets.cpp
#define MM_OUT "tmp/fig_05_dwt_wavelets.png"
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <cmath>
#include <algorithm>
#include "morph.hpp"

//| label: fig-05-dwt-wavelets
//| fig-cap: "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 com base no padrão sintético."
//| echo: true
//| output: true

// Garante que img_gray e img_float utilizem o mesmo padrão sintético claro

// Função auxiliar para visualizar subbandas (normalizar para 0-255)
static cv::Mat sb_vis(const cv::Mat& sb) {
    cv::Mat normalized;
    cv::normalize(sb, normalized, 0, 255, cv::NORM_MINMAX, CV_8U);
    return normalized;
}

// Função para gerar imagem sintética (fallback)
static cv::Mat gerar_imagem_sintetica(int N = 256) {
    cv::Mat img = cv::Mat::zeros(N, N, CV_64F);
    for (int y = 0; y < N; y++) {
        for (int x = 0; x < N; x++) {
            double v = 55 + 35 * ((double)x / N) + 15 * std::sin(y / 24.0);
            if (x > 24 && x < 100 && y > 24 && y < 100) v = 225;
            double cx = 190, cy = 76, r = 34;
            if ((x - cx) * (x - cx) + (y - cy) * (y - cy) < r * r) v = 205;
            if (y > 164 && y < 244) {
                int p = 12;
                v = (((x / p) + (y / p)) % 2 == 0) ? 185 : 65;
            }
            if (std::abs(x - y) < 4) v = 240;
            img.at<double>(y, x) = std::max(0.0, std::min(v, 255.0));
        }
    }
    cv::Mat img_u8;
    img.convertTo(img_u8, CV_8U);
    return img_u8;
}

int main() {
    // Gera imagem sintética
    cv::Mat img_gray = gerar_imagem_sintetica(256);

    cv::Mat img_float;
    img_gray.convertTo(img_float, CV_64F);

    std::vector<std::string> wavelets_comp = {"haar", "db4", "sym4", "bior2.2"};
    std::vector<mm::Image> imgs_comp;
    std::vector<std::string> titles_comp;

    for (const std::string& wname : wavelets_comp) {
        // Decomposição DWT 2D
        mm::Subbands s = mm::dwt2(img_float, wname);
        cv::Mat LL = s.LL;
        cv::Mat HH = s.HH;

        // Visualização das subbandas
        cv::Mat LL_vis = sb_vis(LL);
        cv::Mat HH_vis = sb_vis(HH);

        // Converter para mm::Image para exibição
        mm::Image img_ll(LL_vis.rows, LL_vis.cols, LL_vis.channels());
        std::memcpy(img_ll.data.data(), LL_vis.data, img_ll.data.size());
        mm::Image img_hh(HH_vis.rows, HH_vis.cols, HH_vis.channels());
        std::memcpy(img_hh.data.data(), HH_vis.data, img_hh.data.size());

        imgs_comp.push_back(img_ll);
        imgs_comp.push_back(img_hh);
        titles_comp.push_back(wname + " — LL₁");
        titles_comp.push_back(wname + " — HH₁");
    }

    // Exibir resultados
    std::vector<mm::Image> imgs_comp_display = imgs_comp;
    mm::show(imgs_comp_display, MM_OUT, titles_comp, 4);

    return 0;
}
Overwriting tmp/fig_05_dwt_wavelets.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_dwt_wavelets.cpp -o tmp/fig_05_dwt_wavelets -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_dwt_wavelets \
  && test -f "tmp/fig_05_dwt_wavelets.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_dwt_wavelets.png"
[1] haar — LL₁
[2] haar — HH₁
[3] db4 — LL₁
[4] db4 — HH₁
[5] sym4 — LL₁
[6] sym4 — HH₁
[7] bior2.2 — LL₁
[8] bior2.2 — HH₁
try:
    mm.show(mm.read("tmp/fig_05_dwt_wavelets.png"), figsize=(14, 8))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_dwt_wavelets.png (ver a versao Python)")
Figura 5.21: 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 com base no padrão sintético.

5.6.4.3 Limiarização de Coeficientes e Compressão

Uma das principais aplicações da Transformada Wavelet Discreta (DWT) é a compressão de dados, impulsionada pela capacidade de representação esparsas dos coeficientes. A Figura 5.22 ilustra o efeito da limiarização abrupta (hard thresholding), técnica na qual coeficientes de detalhe com magnitude inferior a um limiar \(T\) são integralmente anulados antes do processo de síntese realizado pela Transformada Wavelet Discreta Inversa (IDWT).

À medida que o limiar \(T\) é elevado, um volume crescente de coeficientes de alta frequência é zerado. Por concentrarem menor energia, a remoção dessas componentes reduz consideravelmente a quantidade de informação necessária para representar a imagem, mantendo a componente de aproximação global (a subbanda \(LL\) mais profunda) intacta para preservar a estrutura macro. Visualmente, esse descarte de coeficientes manifesta-se através do desaparecimento progressivo de texturas finas e da suavização de transições abruptas de intensidade.

A fidelidade da imagem reconstruída frente à original é quantificada pela métrica de Pico da Relação Sinal-Ruído (PSNR, Peak Signal-to-Noise Ratio), expressa em decibéis (dB). Valores mais altos de PSNR indicam menor distorção e maior proximidade matemática com o sinal original. O experimento prático evidencia o decaimento gradual do PSNR conforme a agressividade da limiarização aumenta, permitindo avaliar numericamente o limiar ótimo para o balanço entre compressão e degradação visual.

%%writefile tmp/fig_05_dwt_reconstrucao.cpp
#define MM_OUT "tmp/fig_05_dwt_reconstrucao.png"
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <cmath>
#include <algorithm>
#include <iostream>
#include "morph.hpp"

//| label: fig-05-dwt-reconstrucao
//| fig-cap: "Reconstrução *wavelet* com limiarização de coeficientes (*hard thresholding*): à medida que o limiar aumenta, mais detalhes são zerados, produzindo imagens progressivamente mais suaves. Métrica PSNR quantifica a perda de qualidade sobre o padrão sintético."
//| echo: true
//| output: true

// Função para gerar a imagem sintética
cv::Mat gerar_imagem_sintetica(int N = 256) {
    cv::Mat img = cv::Mat::zeros(N, N, CV_64F);
    for (int y = 0; y < N; y++) {
        for (int x = 0; x < N; x++) {
            double v = 55 + 35 * ((double)x / N) + 15 * std::sin(y / 24.0);
            if (24 < x && x < 100 && 24 < y && y < 100) v = 225;
            double cx = 190, cy = 76, r = 34;
            if ((x - cx)*(x - cx) + (y - cy)*(y - cy) < r*r) v = 205;
            if (y > 164 && y < 244) {
                int p = 12;
                v = (((x / p + y / p) % 2 == 0) ? 185 : 65);
            }
            if (std::abs(x - y) < 4) v = 240;
            img.at<double>(y, x) = std::max(0.0, std::min(255.0, v));
        }
    }
    img.convertTo(img, CV_8U);
    return img;
}

// Função para aplicar hard thresholding em uma matriz
cv::Mat apply_hard_threshold(const cv::Mat& sb, double threshold) {
    cv::Mat result = sb.clone();
    for (int i = 0; i < result.rows; i++) {
        for (int j = 0; j < result.cols; j++) {
            double val = result.at<double>(i, j);
            if (std::abs(val) < threshold) {
                result.at<double>(i, j) = 0.0;
            }
        }
    }
    return result;
}

// Função para reconstruir após decomposição wavelet e thresholding
cv::Mat dwt_threshold_reconstruct(const cv::Mat& img, const std::string& wavelet = "db4", int nivel = 2, double threshold = 0.0) {
    // Decompõe a imagem usando wavedec2
    cv::Mat img_float;
    img.convertTo(img_float, CV_64F);
    mm::WaveDec2 coefs = mm::wavedec2(img_float, wavelet, nivel);

    // Copia o LL final (não é limiarizado)
    std::vector<cv::Mat> coefs_t_ll;
    coefs_t_ll.push_back(coefs.LL);

    // Aplica hard thresholding a todos os detalhes
    std::vector<std::vector<cv::Mat>> coefs_t_details;
    for (int j = 0; j < nivel; j++) {
        std::vector<cv::Mat> detail_thr;
        detail_thr.push_back(apply_hard_threshold(coefs.detail[j][0], threshold)); // LH
        detail_thr.push_back(apply_hard_threshold(coefs.detail[j][1], threshold)); // HL
        detail_thr.push_back(apply_hard_threshold(coefs.detail[j][2], threshold)); // HH
        coefs_t_details.push_back(detail_thr);
    }

    // Reconstrução via waverec2
    mm::WaveDec2 coefs_t;
    coefs_t.LL = coefs_t_ll[0];
    coefs_t.detail = coefs_t_details;
    cv::Mat rec = mm::waverec2(coefs_t, wavelet);

    // Recorte para dimensão original
    rec = rec(cv::Rect(0, 0, img.cols, img.rows)).clone();

    // Clip e conversão para uint8
    cv::Mat rec_clipped;
    cv::Mat rec_float;
    rec.convertTo(rec_float, CV_64F);
    cv::max(0.0, rec_float, rec_float);
    cv::min(255.0, rec_float, rec_float);
    rec_float.convertTo(rec_clipped, CV_8U);
    return rec_clipped;
}

int main() {
    // Garante que img_gray utilize o mesmo padrão sintético claro
    cv::Mat img_gray;
    // Fallback caso o bloco anterior não tenha sido executado na mesma sessão
    img_gray = gerar_imagem_sintetica(256);

    std::vector<double> thresholds = {0, 10, 30, 60, 100};
    std::vector<cv::Mat> imgs_thr;
    imgs_thr.push_back(img_gray);
    std::vector<std::string> titles_thr;
    titles_thr.push_back("Original");

    for (double t : thresholds) {
        cv::Mat rec = dwt_threshold_reconstruct(img_gray, "db4", 2, t);
        double psnr = cv::PSNR(img_gray, rec);
        imgs_thr.push_back(rec);

        // Formata o título com PSNR
        char buf[50];
        snprintf(buf, sizeof(buf), "T=%.0f  PSNR=%.1f dB", t, psnr);
        titles_thr.push_back(std::string(buf));
    }

    // Converte para mm::Image para exibição
    std::vector<mm::Image> imgs_out;
    for (const cv::Mat& m : imgs_thr) {
        mm::Image mm_img(m.rows, m.cols, 1);
        std::memcpy(mm_img.data.data(), m.data, mm_img.data.size());
        imgs_out.push_back(mm_img);
    }

    mm::show(imgs_out, MM_OUT, titles_thr, 3);

    return 0;
}
Overwriting tmp/fig_05_dwt_reconstrucao.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_dwt_reconstrucao.cpp -o tmp/fig_05_dwt_reconstrucao -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_dwt_reconstrucao \
  && test -f "tmp/fig_05_dwt_reconstrucao.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_dwt_reconstrucao.png"
[1] Original
[2] T=0  PSNR=361.2 dB
[3] T=10  PSNR=42.4 dB
[4] T=30  PSNR=32.5 dB
[5] T=60  PSNR=28.0 dB
[6] T=100  PSNR=23.1 dB
try:
    mm.show(mm.read("tmp/fig_05_dwt_reconstrucao.png"), figsize=(14, 10))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_dwt_reconstrucao.png (ver a versao Python)")
Figura 5.22: Reconstrução wavelet com limiarização de coeficientes (hard thresholding): à medida que o limiar aumenta, mais detalhes são zerados, produzindo imagens progressivamente mais suaves. Métrica PSNR quantifica a perda de qualidade sobre o padrão sintético.

Síntese — Fourier vs. Wavelets: quando utilizar cada abordagem?

A Tabela 5.5 sintetiza as principais diferenças estruturais e operacionais entre a Transformada Discreta de Fourier (DFT) e a Transformada Wavelet Discreta (DWT).

Tabela 5.5: Comparação entre a Transformada Discreta de Fourier (DFT) e a Transformada Wavelet Discreta (DWT), destacando suas principais características e aplicações.
Critério Fourier (DFT) Wavelet (DWT)
Funções de base Senoides de suporte infinito Funções de suporte compacto
Localização espacial Não explícita (global) Explícita (local)
Filtragem espectral Excelente para controle fino de frequências Baseada em subbandas (escalas)
Compressão de imagens Base da DCT (JPEG tradicional) Base da DWT (JPEG 2000)
Análise multiescala Não Sim
Remoção de ruído periódico Altamente eficiente Pouco indicada
Sinais não estacionários Limitada Altamente eficiente

Em termos práticos, a DFT consolida-se como a ferramenta ideal para análise espectral pura, projeto de filtros seletivos no domínio da frequência e atenuação de ruídos periódicos e harmônicos. Por outro lado, a DWT sobressai-se em cenários que exigem a preservação rigorosa da localização espacial das feições associada ao seu conteúdo frequencial, destacando-se em compressão de dados, análise multirresolução e processamento de transições abruptas. Desse modo, ambas as transformadas devem ser compreendidas como técnicas perfeitamente complementares, mapeando caminhos distintos e específicos para a resolução de problemas em PDI-VC.

NotaAnalogias com Áudio: Limitações e Cuidados

Ao fazer analogias entre processamento de imagens e áudio, é importante considerar as diferenças fundamentais:

  • Em sistemas de áudio estéreo/multicanais, a fase entre canais é crucial para a percepção de localização espacial (diferenças interaurais de fase e tempo).

  • Em sistemas monaurais, a fase tem influência perceptual limitada — o ouvido humano é relativamente insensível à fase absoluta de componentes senoidais isolados.

  • Em imagens, a fase da DFT é sempre fundamental para a localização espacial de estruturas, independentemente de ser uma imagem monocromática ou colorida.

A analogia entre fase em áudio e fase em imagens deve ser usada com cautela, destacando que, embora ambas carreguem informações sobre a organização espacial/temporal do sinal, os mecanismos perceptuais são fundamentalmente diferentes.

5.7 Compressão de Imagens

Enquanto as wavelets estabelecem a fundação teórica do padrão JPEG 2000, o padrão JPEG tradicional baseia-se na Transformada Discreta de Cossenos (DCT, Discrete Cosine Transform). Apesar das diferenças estruturais, ambas as abordagens compartilham o mesmo princípio fundamental: compactar a energia da imagem em um número reduzido de coeficientes e descartar as componentes de menor relevância com impacto visual mínimo.

O objetivo central da compressão é reduzir o volume de dados necessário para o armazenamento ou transmissão de uma imagem. Esse processo é viabilizado pela identificação e eliminação de redundâncias estruturais e perceptuais.

5.7.1 Taxonomia das Redundâncias

O desenvolvimento de algoritmos de compressão fundamenta-se na identificação e eliminação de três categorias principais de redundância, sintetizadas na Tabela 5.6.

Tabela 5.6: Categorias de redundância em imagens digitais e seus respectivos mecanismos de exploração.
Tipo Definição Abordagem de Exploração
Espacial (interpixel) Alta correlação e dependência estatística entre pixels vizinhos. DCT, DWT e codificação preditiva.
Espectral (intercanal) Correlação estatística entre os canais de cor de uma mesma imagem. Transformações de espaço de cor (ex: RGB para \(YC_bC_r\)).
Psicovisual Insensibilidade do sistema visual humano (SVH) a variações de alta frequência e baixo contraste. Processos de quantização seletiva de coeficientes.

A depender da preservação da informação original após o processo de decodificação, os métodos de compressão dividem-se em duas classes fundamentais:

  • Sem perda (lossless): Garante uma reconstrução bit a bit idêntica à imagem original. É empregada em cenários onde a integridade dos dados é estritamente crítica, como em imagens médicas, diagnósticos por imagem e armazenamento de documentos textuais.
  • Com perda (lossy): Admite a introdução de uma distorção controlada no sinal em troca de taxas de compressão substancialmente mais elevadas. É a abordagem padrão para fotografias de consumo e streaming de vídeo, ecossistemas nos quais o SVH tolera pequenas atenuações de alta frequência sem percepção de degradação da qualidade visual.

5.7.2 Transformada de Cossenos Discreta (DCT-II 2D)

A Transformada de Cossenos Discreta (DCT) constitui a operação central do padrão JPEG. Diferentemente da DFT, que utiliza uma base complexa, a DCT baseia-se em funções trigonométricas puramente reais. Para um bloco de imagem \(f(x,y)\) de dimensões \(N \times N\), a DCT-II 2D mapeia o sinal espacial para o domínio das frequências espaciais, gerando a matriz de coeficientes \(C(u,v)\) por meio de:

\[ 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 os fatores de normalização ortogonal são dados por \(\alpha(0) = \sqrt{1/N}\) e \(\alpha(k) = \sqrt{2/N}\) para \(k > 0\).

Cada coeficiente \(C(u,v)\) quantifica a contribuição — ou “peso” — de uma frequência espacial específica dentro daquele bloco. O termo \(C(0,0)\) é denominado componente DC e representa a intensidade média do bloco (frequência nula). Os demais coeficientes, chamados de componentes AC (Alternating Current), correspondem às frequências espaciais progressivamente maiores.

5.7.3 As Funções de Base da DCT

Sob uma perspectiva geométrica, a Equação 5.9 realiza a projeção do bloco de pixels sobre um conjunto de funções ortogonais. Para o caso padrão do JPEG (\(N=8\)), o bloco espacial é decomposto em uma combinação linear de 64 funções de base bidimensionais, denotadas por \(B_{u,v}(x,y)\) e geradas pelo produto de funções cossenoidais:

\[B_{u,v}(x,y) = \cos\left[ \frac{\pi (2x+1)u}{16} \right] \cos\left[ \frac{\pi (2y+1)v}{16} \right]\]

Dessa forma, a operação inversa pode ser interpretada como a reconstrução exata do bloco original por meio da soma ponderada dessas 64 matrizes de base, onde cada coeficiente \(C(u,v)\) atua como o peso analítico de sua respectiva componente harmônica.

A frequência espacial indicada pelos índices \((u,v)\) determina o número de ciclos de oscilação ao longo das dimensões horizontais e verticais do bloco. Como ilustrado na Figura 5.23 — cujo código isola cada base aplicando a transformação inversa sobre impulsos unitários —, essas 64 funções são organizadas em uma matriz \(8 \times 8\). O canto superior esquerdo (\(u=0, v=0\)) exibe o padrão uniforme de frequência nula (DC), enquanto o avanço para a direita (eixo \(u\)) ou para baixo (eixo \(v\)) mapeia variações harmônicas progressivamente maiores, representando transições rápidas, bordas e texturas nas orientações horizontais, verticais e diagonais.

NotaDCT vs DFT: Vantagem da Compactação de Energia

Tanto a DCT quanto a DFT mapeiam um bloco \(N \times N\) espacial em uma matriz de coeficientes de mesma dimensão. Contudo, para imagens naturais, a DCT apresenta maior eficiência na compactação de energia nas baixas frequências. Isso ocorre porque a DCT assume implicitamente uma simetria par do sinal nas fronteiras do bloco, o que equivale a uma extensão periódica contínua, minimizando o efeito de espalhamento espectral (ringing). Como consequência, a maioria dos coeficientes AC decai rapidamente para valores próximos de zero, otimizando o pipeline de compressão sem introduzir degradação visual perceptível.

%%writefile tmp/fig_05_dct_basis.cpp
#define MM_OUT "tmp/fig_05_dct_basis.png"
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <cstring>
#include "morph.hpp"
#include <filesystem>

//| label: fig-05-dct-basis
//| fig-cap: "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."
//| echo: true
//| output: true

int main() {
    // Cada base é a IDCT de um único coeficiente unitário — montadas num mosaico 8×8.
    int tile = 32;
    cv::Mat montagem = cv::Mat::zeros(8 * tile, 8 * tile, CV_8U);

    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            cv::Mat coef = cv::Mat::zeros(8, 8, CV_64F);
            coef.at<double>(i, j) = 1.0;

            cv::Mat base = mm::idct2(coef);
            cv::Mat base_8u;
            cv::normalize(base, base_8u, 0, 255, cv::NORM_MINMAX, CV_8U);

            cv::Mat base_resized;
            cv::resize(base_8u, base_resized, cv::Size(tile, tile), 0, 0, cv::INTER_NEAREST);

            // Copiar a base redimensionada para o mosaico
            cv::Mat roi = montagem(cv::Rect(j * tile, i * tile, tile, tile));
            base_resized.copyTo(roi);
        }
    }

    std::vector<mm::Image> images;
    mm::Image result(montagem.rows, montagem.cols, montagem.channels());
    std::memcpy(result.data.data(), montagem.data, result.data.size());
    images.push_back(result);

    std::vector<std::string> titles = {"As 64 bases da DCT-II 8×8 (DC no topo-esquerdo)"};
    mm::show(images, MM_OUT, titles, 1);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(montagem, "tmp/fig_05_dct_basis_0.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_dct_basis.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_dct_basis.cpp -o tmp/fig_05_dct_basis -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_dct_basis \
  && test -f "tmp/fig_05_dct_basis.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_dct_basis.png"
[1] As 64 bases da DCT-II 8×8 (DC no topo-esquerdo)
try:
    mm.show(
        [
            mm.read("tmp/fig_05_dct_basis_0.png"),
        ],
        titles=[
            'As 64 bases da DCT-II 8×8 (DC no topo-esquerdo)',
        ],
        cols=1,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_dct_basis_0.png (ver a versao Python)")
Figura 5.23: 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.4 Concentração de Energia e Reconstrução Progressiva

Antes da aplicação da DCT, os pixels do bloco de intensidade são rotineiramente transladados (subtraindo-se \(128\) para imagens de 8 bits) a fim de centralizar o sinal em torno de zero, eliminando componentes contínuas desnecessárias. Ao computar a DCT sobre o bloco resultante, a propriedade de compactação de energia torna-se evidente: a quase totalidade da variância e da informação da imagem original concentra-se no coeficiente DC (\(C(0,0)\)) e nos primeiros harmônicos AC de baixa frequência.

A Figura 5.24 demonstra esse fenômeno por meio de uma reconstrução progressiva por truncamento abrupto. Em vez de utilizar todos os 64 coeficientes, o algoritmo preserva apenas os \(k\) primeiros componentes — selecionados com base em uma varredura que prioriza as baixas frequências espaciais — e anula os demais.

A síntese inversa (IDCT) realizada com apenas uma fração dos coeficientes (como 15% ou 30%) já é capaz de recuperar as estruturas e a iluminação macro do bloco original de pixels. À medida que harmônicos de frequências mais altas são progressivamente reincorporados, os detalhes finos e as transições rápidas são restaurados. Esse comportamento valida o princípio da compressão perceptual: as altas frequências descartadas possuem pouca energia e sua ausência, em condições normais, gera um impacto visual secundário na percepção do observador.

%%writefile tmp/fig_05_dct_bloco.cpp
#define MM_OUT "tmp/fig_05_dct_bloco.png"
// Compile: g++ -std=c++17 -o program main.cpp $(pkg-config --cflags --libs opencv4)

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <string>
#include <cmath>
#include <algorithm>
#include "morph.hpp"

//| label: fig-05-dct-bloco
//| fig-cap: "DCT 2D em bloco 8×8: coeficientes e reconstrução progressiva."
//| echo: true
//| output: true

// Função para DCT-II 2D ortogonal (separável)
cv::Mat dct2(const cv::Mat& bloco) {
    cv::Mat temp, result;
    cv::dct(bloco, temp);  // DCT nas linhas
    cv::dct(temp.t(), result);  // DCT nas colunas
    return result.t();
}

// Função para IDCT-II 2D ortogonal
cv::Mat idct2(const cv::Mat& coefs) {
    cv::Mat temp, result;
    cv::idct(coefs, temp);  // IDCT nas linhas
    cv::idct(temp.t(), result);  // IDCT nas colunas
    return result.t();
}

// Função para ordenar índices por soma (zig-zag simplificado)
std::vector<std::pair<int,int>> getZigZagIndices() {
    std::vector<std::pair<int,int>> indices;
    for (int u = 0; u < 8; u++) {
        for (int v = 0; v < 8; v++) {
            indices.push_back({u, v});
        }
    }
    std::sort(indices.begin(), indices.end(), 
              [](const std::pair<int,int>& a, const std::pair<int,int>& b) {
                  int sum_a = a.first + a.second;
                  int sum_b = b.first + b.second;
                  if (sum_a != sum_b) return sum_a < sum_b;
                  // Para mesma soma, prioriza diagonal secundária
                  return a.first > b.first;
              });
    return indices;
}

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_20.png");
// [pdi:state-io:end]

    // img_gray é fornecido automaticamente como mm::Image

    // Converter mm::Image para cv::Mat
    cv::Mat img_mat(img_gray.h, img_gray.w, CV_8UC1, img_gray.data.data());

    // ── Bloco 8×8 centralizado da imagem ─────────────────────────────────────────
    int cy = img_gray.h / 2;
    int cx = img_gray.w / 2;

    cv::Mat bloco_raw = img_mat(cv::Rect(cx, cy, 8, 8)).clone();
    cv::Mat bloco_float;
    bloco_raw.convertTo(bloco_float, CV_32F);
    bloco_float -= 128.0f;  // Centralizar em torno de zero

    // Aplicar DCT 2D
    cv::Mat C = dct2(bloco_float);

    std::cout << "Coeficientes DCT do bloco 8×8:" << std::endl;
    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            std::cout << (int)std::round(C.at<float>(i, j)) << " ";
        }
        std::cout << std::endl;
    }

    // Calcular energias
    double energia_dc = C.at<float>(0, 0) * C.at<float>(0, 0);
    double energia_total = 0;
    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            energia_total += C.at<float>(i, j) * C.at<float>(i, j);
        }
    }

    std::cout << "\nEnergia DC     : " << energia_dc << std::endl;
    std::cout << "Energia total  : " << energia_total << std::endl;
    std::cout << "Fração no DC   : " << (energia_dc / energia_total) * 100 << "% ← concentração de energia" << std::endl;

    // ── Reconstrução progressiva ──────────────────────────────────────────────────
    std::vector<mm::Image> imgs_rec;
    std::vector<std::string> titles_rec;

    // Bloco original
    cv::Mat bloco_original;
    bloco_raw.convertTo(bloco_original, CV_8U);
    mm::Image img_orig(8, 8, 1);
    std::memcpy(img_orig.data.data(), bloco_original.data, 64);
    imgs_rec.push_back(img_orig);
    titles_rec.push_back("Bloco original\n(8×8 pixels)");

    // Obter índices em ordem zig-zag
    std::vector<std::pair<int,int>> indices = getZigZagIndices();

    // Testar diferentes números de coeficientes
    std::vector<int> keeps = {1, 4, 10, 20, 40, 64};
    for (int keep : keeps) {
        cv::Mat C_trunc = cv::Mat::zeros(8, 8, CV_32F);
        for (int k = 0; k < keep; k++) {
            int u = indices[k].first;
            int v = indices[k].second;
            C_trunc.at<float>(u, v) = C.at<float>(u, v);
        }

        // Reconstruir bloco
        cv::Mat rec_float = idct2(C_trunc);
        rec_float += 128.0f;

        // Clipping e conversão para uint8
        cv::Mat rec;
        rec_float.convertTo(rec, CV_8U);
        cv::threshold(rec, rec, 255, 255, cv::THRESH_TRUNC);

        mm::Image img_rec(8, 8, 1);
        std::memcpy(img_rec.data.data(), rec.data, 64);
        imgs_rec.push_back(img_rec);

        int pct = (int)std::round((keep / 64.0) * 100);
        titles_rec.push_back(std::to_string(keep) + " coef.\n(" + std::to_string(pct) + "% do total)");
    }

    // Exibir resultados
    mm::show(imgs_rec, MM_OUT, titles_rec, 4);

    return 0;
}
Overwriting tmp/fig_05_dct_bloco.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_dct_bloco.cpp -o tmp/fig_05_dct_bloco -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_dct_bloco \
  && test -f "tmp/fig_05_dct_bloco.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_dct_bloco.png"
Coeficientes DCT do bloco 8×8:
12 17 17 16 15 14 11 7 
19 26 23 20 19 17 14 8 
22 29 25 22 21 20 16 8 
22 31 32 35 38 35 24 11 
21 31 37 47 53 46 29 13 
21 31 35 44 52 47 31 13 
19 27 28 35 45 45 34 18 
12 16 16 22 29 33 28 16 

Energia DC     : 143.593
Energia total  : 49504
Fração no DC   : 0.290063% ← concentração de energia
[1] Bloco original
(8×8 pixels)
[2] 1 coef.
(2% do total)
[3] 4 coef.
(6% do total)
[4] 10 coef.
(16% do total)
[5] 20 coef.
(31% do total)
[6] 40 coef.
(63% do total)
[7] 64 coef.
(100% do total)
try:
    mm.show(mm.read("tmp/fig_05_dct_bloco.png"), figsize=(12, 7))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_dct_bloco.png (ver a versao Python)")
Figura 5.24: DCT 2D em bloco 8×8: coeficientes e reconstrução progressiva.

5.7.5 O Pipeline de Compressão JPEG

O padrão JPEG opera dividindo a imagem em blocos disjuntos de \(8 \times 8\) pixels, processados por meio de uma sequência de transformações espaciais, perceptuais e estatísticas. O pipeline completo de codificação é estruturado em seis etapas principais:

\[ \text{RGB} \xrightarrow{\text{(1) } YC_bC_r} \xrightarrow{\text{(2) Subamostragem}} \xrightarrow{\text{(3) Blocos } 8 \times 8} \xrightarrow{\text{(4) DCT}} \xrightarrow{\text{(5) Quantização}} \xrightarrow{\text{(6) Codificação Entrópica}} \]

A Tabela 5.7 detalha a função analítica e o fundamento perceptual que justifica cada uma dessas etapas.

Tabela 5.7: Etapas do pipeline de compressão JPEG e seus respectivos fundamentos de projeto.
Etapa Operação Fundamento Perceptual e Estatístico
1 Conversão \(RGB \rightarrow YC_bC_r\) Separa a luminância (\(Y\)) da crominância (\(C_b, C_r\)). O sistema visual humano (SVH) apresenta maior sensibilidade a variações de brilho do que de cor.
2 Subamostragem de crominância (ex: 4:2:0) Reduz a resolução espacial dos canais de cor pela metade, descartando dados redundantes com impacto visual desprezível.
3–4 Centralização e aplicação da DCT \(8 \times 8\) Translada os pixels para o intervalo \([-128, 127]\) e compacta a energia espectral do bloco nos coeficientes de baixa frequência.
5 Quantização linear seletiva Divide cada coeficiente \(C(u,v)\) pelo elemento correspondente da matriz \(Q(u,v)\), aplicando arredondamento inteiro. Constitui a principal fonte de compressão com perda.
6 Varredura em ziguezague e codificação Ordena os coeficientes quantizados para maximizar sequências nulas consecutivas, otimizando a codificação por comprimento de corrida (RLE) e a codificação de Huffman.

A matriz de quantização \(Q(u,v)\) é o mecanismo central de controle do compromisso entre taxa de compressão e qualidade visual. No algoritmo prático da Figura 5.25, o fator de qualidade estipulado pelo usuário (escala de 1 a 100) é convertido em um escalar que parametriza a severidade da matriz \(Q\). Valores reduzidos de qualidade expandem os divisores de \(Q(u,v)\), forçando o truncamento em massa dos coeficientes AC para zero. Quando essa eliminação é excessiva, a descontinuidade nas fronteiras dos blocos adjacentes não é atenuada na reconstrução, gerando os denominados artefatos de bloco (blocking artifacts).

A Lógica da Varredura em Ziguezague

A eficiência do codificador entrópico subsequente à quantização depende diretamente da ordenação dos dados. Como a DCT concentra a energia vital no vértice superior esquerdo da matriz (baixas frequências) e empurra os coeficientes nulos para as extremidades opostas, a leitura linear por linhas ou colunas fragmentaria as sequências de zeros.

A ordenação em ziguezague soluciona essa limitação ao percorrer a matriz diagonalmente em ordem crescente de frequência espacial. Esse mapeamento agrupa os coeficientes significativos no início do vetor e concentra os coeficientes nulos em uma única sequência contínua ao final do arranjo, permitindo que o algoritmo RLE codifique grandes blocos de dados de forma compacta e eficiente.

NotaO que é RLE?

RLE (Run-Length Encoding) é uma técnica de compressão sem perdas que codifica sequências consecutivas de valores idênticos — especialmente zeros — como um par (contagem, valor). No JPEG, após a varredura em ziguezague, os coeficientes quantizados são organizados de modo que os zeros se concentrem ao final do vetor. O RLE então comprime essa longa corrida de zeros com extrema eficiência, otimizando o armazenamento e a transmissão da imagem comprimida.

%%writefile tmp/fig_05_jpeg_pipeline.cpp
#define MM_OUT "tmp/fig_05_jpeg_pipeline.png"
#include <opencv2/opencv.hpp>
#include <string>
#include <vector>
#include <cstdio>
#include "morph.hpp"

//| label: fig-05-jpeg-pipeline
//| fig-cap: "*Pipeline* JPEG simplificado aplicado à imagem clássica do *Cameraman*: DCT em blocos 8×8, quantização com diferentes fatores de qualidade e reconstrução via IDCT. Os artefatos de bloco (*blocking artifacts*) tornam-se visualmente evidentes em fatores de qualidade reduzidos ($Q=10$ e $Q=25$)."
//| echo: true
//| output: true

int main() {
    // Pipeline JPEG (DCT 8x8 -> quantizacao -> IDCT) via mm.jpegCompress,
    // na imagem classica do Cameraman (asset do capitulo) reduzida a 256x256.
    cv::Mat img_resized;
    cv::Mat img_read = cv::imread("imagens/cameraman.png", cv::IMREAD_GRAYSCALE);
    cv::resize(img_read, img_resized, cv::Size(256, 256));
    mm::Image img_gray_mm = mm::gray(mm::Image(img_resized));
    mm::Image img_src = img_gray_mm;

    std::vector<mm::Image> imgs = {img_src};
    std::vector<std::string> titles = {"Original (Cameraman)"};
    for (int q : {10, 25, 50, 75, 90}) {
        mm::Image rec = mm::jpegCompress(img_src, q);
        imgs.push_back(rec);
        char title_buf[128];
        std::snprintf(title_buf, sizeof(title_buf), "Q=%d (PSNR=%.1f dB)", q, mm::psnr(img_src, rec));
        titles.push_back(std::string(title_buf));
    }

    mm::show(imgs, MM_OUT, titles, 3);

    return 0;
}
Overwriting tmp/fig_05_jpeg_pipeline.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_jpeg_pipeline.cpp -o tmp/fig_05_jpeg_pipeline -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_jpeg_pipeline \
  && test -f "tmp/fig_05_jpeg_pipeline.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_jpeg_pipeline.png"
[1] Original (Cameraman)
[2] Q=10 (PSNR=28.0 dB)
[3] Q=25 (PSNR=30.7 dB)
[4] Q=50 (PSNR=32.8 dB)
[5] Q=75 (PSNR=35.2 dB)
[6] Q=90 (PSNR=40.0 dB)
try:
    mm.show(mm.read("tmp/fig_05_jpeg_pipeline.png"), figsize=(14, 10))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_jpeg_pipeline.png (ver a versao Python)")
Figura 5.25: Pipeline JPEG simplificado aplicado à imagem clássica do Cameraman: DCT em blocos 8×8, quantização com diferentes fatores de qualidade e reconstrução via IDCT. Os artefatos de bloco (blocking artifacts) tornam-se visualmente evidentes em fatores de qualidade reduzidos (\(Q=10\) e \(Q=25\)).

5.7.6 Simulador Interativo: Quantização DCT

O simulador da Figura 5.26 permite explorar o impacto do processo de quantização sobre um bloco \(8 \times 8\) extraído de uma imagem real, sintetizando em tempo real as seguintes componentes:

  • Bloco original e reconstruído: Representação direta dos pixels no domínio espacial em escala de cinza [0, 255].
  • Coeficientes DCT: Distribuição da energia mapeada de forma logarítmica em um gradiente cromático, evidenciando a concentração de intensidade no vértice superior esquerdo (baixas frequências).
  • Coeficientes quantizados: Exibição dos valores inteiros resultantes da divisão pela matriz \(Q(u,v)\), tornando visualmente explícito o surgimento em massa de coeficientes nulos (em tons escuros) conforme o fator de qualidade é reduzido.
  • Métricas de compressão: Painel de monitoramento que quantifica o Erro Quadrático Médio (MSE), o número de coeficientes preservados e o volume de zeros gerados para a codificação entrópica.
⊞ 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.26: 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 de um formato de armazenamento digital impacta diretamente o compromisso entre qualidade visual, tamanho de arquivo e custo computacional de decodificação. Os três formatos de maior relevância para arquiteturas web e sistemas de computação visual são o JPEG, o PNG e o WebP.

5.8.1 Características dos Formatos

A Tabela 5.8 sintetiza as propriedades estruturais dos principais formatos de imagem rasterizados.

Tabela 5.8: Comparação estrutural entre os principais formatos de imagem rasterizados.
Característica JPEG PNG WebP
Compressão Com perda Sem perda Com e sem perda.
Transparência (canal alfa) Não Sim Sim.
Suporte a animação Não Limitado (APNG) Sim.
Algoritmo base DCT + Huffman DEFLATE (LZ77 + Huffman) VP8 / VP8L.
Melhor para Fotografia Gráficos, texto e ícones Uso universal em ambiente Web.
Pior para Texto e bordas nítidas Imagens fotográficas complexas Compatibilidade legada.

5.8.2 Métricas de Avaliação de Qualidade

Duas métricas objetivas são amplamente adotadas para quantificar a distorção introduzida por processos de compressão:

Pico da Relação Sinal-Ruído (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 quantizadas em 8 bits e \(\text{MSE}\) representa o Erro Quadrático Médio (Mean Squared Error). Valores de PSNR acima de 40 dB indicam excelente fidelidade; entre 30 dB e 40 dB representam boa qualidade; e valores inferiores a 30 dB correspondem a degradações visuais facilmente perceptíveis.

Índice de Similaridade Estrutural (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 avalia janelas locais da imagem com base em três componentes complementares: luminância (\(\mu_f, \mu_g\)), contraste (\(\sigma_f, \sigma_g\)) e estrutura (\(\sigma_{fg}\)), ponderados por constantes de estabilidade \(c_1\) e \(c_2\). O índice varia no intervalo \([-1, 1]\), onde a unidade representa a identidade perfeita. Ao contrário do PSNR, o SSIM considera a organização espacial dos erros, alinhando-se à percepção do sistema visual humano (SVH).

NotaPSNR vs SSIM: Aplicação de Métricas Perceptuais

O PSNR possui formulação matemática simples e baixo custo computacional, contudo, tende a superestimar a qualidade em imagens com distorções localizadas ou subestimá-la em variações globais de brilho toleradas pelo observador. O SSIM modela com maior fidelidade a percepção biológica, mas exige maior esforço de processamento. Para análises rigorosas de codificadores, recomenda-se reportar ambas as métricas estatísticas em caráter complementar.

5.8.3 Inspeção Visual: Natureza dos Artefatos de Compressão

A natureza matemática do codificador dita o tipo de degradação introduzida em taxas de bits reduzidas. Conforme ilustrado na Figura 5.27, a compressão agressiva via DCT no padrão JPEG segmenta a imagem em malhas rígidas, gerando os artefatos de bloco (blocking artifacts). Em contrapartida, algoritmos baseados em codificação preditiva ou representações submetidas a transformadas espaciais avançadas (como o WebP e o JPEG 2000) eliminam as descontinuidades de bloco, mas introduzem perda de textura fina e borramentos característicos ao redor de bordas de alto contraste.

%%writefile tmp/fig_05_zoom_artefatos.cpp
#define MM_OUT "tmp/fig_05_zoom_artefatos.png"
// Compile: g++ -std=c++17 -o main main.cpp `pkg-config --cflags --libs opencv4`
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include "morph.hpp"
#include <filesystem>

#ifndef MM_OUT
#endif

//| label: fig-05-zoom-artefatos
//| fig-cap: "Análise comparativa de artefatos de compressão sob fator de qualidade reduzido ($Q=10$). À esquerda, observa-se o artefato de bloco característico da discretização por DCT no JPEG. À direita, evidencia-se o efeito de atenuação e suavização de bordas intrínseco ao padrão WebP."
//| echo: true
//| output: true

int main() {
    // Carrega a imagem e converte para escala de cinza
    mm::Image img_loaded = mm::read("imagens/cameraman.png");
    cv::Mat img_src_cv = img_loaded;
    cv::Mat img_resized;
    cv::resize(img_src_cv, img_resized, cv::Size(256, 256));
    mm::Image img_src = mm::gray(cv::Mat(img_resized));

    // Salva com baixa qualidade JPEG e WebP
    std::vector<int> jpeg_params;
    jpeg_params.push_back(cv::IMWRITE_JPEG_QUALITY);
    jpeg_params.push_back(10);
    std::vector<int> webp_params;
    webp_params.push_back(cv::IMWRITE_WEBP_QUALITY);
    webp_params.push_back(10);

    cv::Mat img_src_mat = img_src;
    cv::imwrite("tmp/zoom_q10.jpg", img_src_mat, jpeg_params);
    cv::imwrite("tmp/zoom_q10.webp", img_src_mat, webp_params);

    // Função de zoom
    auto zoom = [](cv::Mat& img) {
        cv::Mat roi = img(cv::Range(120, 200), cv::Range(150, 230));
        cv::Mat resized;
        cv::resize(roi, resized, cv::Size(320, 320), 0, 0, cv::INTER_NEAREST);
        return resized;
    };

    // Lê as imagens comprimidas
    cv::Mat img_src_mat2 = img_src;
    cv::Mat jpeg_img = cv::imread("tmp/zoom_q10.jpg", cv::IMREAD_GRAYSCALE);
    cv::Mat webp_img = cv::imread("tmp/zoom_q10.webp", cv::IMREAD_GRAYSCALE);

    // Aplica zoom
    cv::Mat z_orig_cv = zoom(img_src_mat2);
    cv::Mat z_jpeg_cv = zoom(jpeg_img);
    cv::Mat z_webp_cv = zoom(webp_img);

    // Converte para mm::Image para exibição
    mm::Image z_orig = z_orig_cv;
    mm::Image z_jpeg = z_jpeg_cv;
    mm::Image z_webp = z_webp_cv;

    std::vector<mm::Image> images = {z_orig, z_jpeg, z_webp};
    std::vector<std::string> titles = {
        "Zoom Original", 
        "JPEG Q=10 (Artefato de Bloco)", 
        "WebP Q=10 (Suavizacao)"
    };
    mm::show(images, MM_OUT, titles, 3);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(z_orig, "tmp/fig_05_zoom_artefatos_0.png");
mm::write(z_jpeg, "tmp/fig_05_zoom_artefatos_1.png");
mm::write(z_webp, "tmp/fig_05_zoom_artefatos_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_zoom_artefatos.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_zoom_artefatos.cpp -o tmp/fig_05_zoom_artefatos -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_zoom_artefatos \
  && test -f "tmp/fig_05_zoom_artefatos.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_zoom_artefatos.png"
[1] Zoom Original
[2] JPEG Q=10 (Artefato de Bloco)
[3] WebP Q=10 (Suavizacao)
try:
    mm.show(
        [
            mm.read("tmp/fig_05_zoom_artefatos_0.png"),
            mm.read("tmp/fig_05_zoom_artefatos_1.png"),
            mm.read("tmp/fig_05_zoom_artefatos_2.png"),
        ],
        titles=[
            'Zoom Original',
            'JPEG Q=10 (Artefato de Bloco)',
            'WebP Q=10 (Suavizacao)',
        ],
        cols=3,
        figsize=(14, 5),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_zoom_artefatos_0.png (ver a versao Python)")
Figura 5.27: Análise comparativa de artefatos de compressão sob fator de qualidade reduzido (\(Q=10\)). À esquerda, observa-se o artefato de bloco característico da discretização por DCT no JPEG. À direita, evidencia-se o efeito de atenuação e suavização de bordas intrínseco ao padrão WebP.

5.8.4 Avaliação Quantitativa e Espacial da Compressão

A validação dos algoritmos de compressão com perda exige uma análise que correlacione o custo de armazenamento à fidelidade do sinal reconstruído. Essa avaliação é realizada de forma complementar através de curvas de desempenho global e pelo mapeamento local das distorções induzidas pelos codificadores.

5.8.4.1 Curvas de Taxa-Distorção

A Figura 5.28 apresenta a avaliação empírica do pipeline JPEG e WebP por meio de curvas de taxa-distorção, que monitoram o ganho de compressão (tamanho do arquivo em KB) em função do PSNR. O formato PNG atua como linha de base ideal (\(\text{PSNR} = \infty\)), pois sua natureza lossless impede qualquer degradação, embora demande um volume de dados substancialmente maior.

A análise das curvas demonstra a superioridade e a eficiência do padrão WebP sobre o JPEG tradicional: para atingir um mesmo patamar de fidelidade matemática (como a faixa de excelente qualidade, onde \(\text{PSNR} > 40\text{ dB}\)), o codificador WebP gera arquivos significativamente menores. Esse comportamento traduz o impacto prático da evolução dos algoritmos na otimização de sistemas de transmissão e armazenamento digital.

%%writefile tmp/fig_05_formatos_comparacao.cpp
#define MM_OUT "tmp/fig_05_formatos_comparacao.png"
//| label: fig-05-formatos-comparacao
//| fig-cap: "Curva taxa-distorção: PSNR vs tamanho de arquivo para JPEG, WebP e PNG aplicada à imagem do *Cameraman*."
//| echo: true
//| output: true

#include <opencv2/opencv.hpp>
#include <filesystem>
#include <vector>
#include <string>
#include <iostream>
#include "morph.hpp"

int main() {
    // Cria diretório tmp se não existir
    std::filesystem::create_directories("tmp");

    // Lê e redimensiona a imagem
    cv::Mat img = cv::imread("imagens/cameraman.png", cv::IMREAD_GRAYSCALE);
    cv::Mat src;
    cv::resize(img, src, cv::Size(256, 256));

    // Converte para mm::Image para compatibilidade
    mm::Image src_mm(src);

    std::vector<double> jpeg_kb, jpeg_psnr;
    std::vector<int> jpeg_qualities = {10, 20, 30, 40, 50, 60, 70, 80, 90, 95};
    for (int q : jpeg_qualities) {
        std::string p = "tmp/fmt_q" + std::to_string(q) + ".jpg";
        std::vector<int> params = {cv::IMWRITE_JPEG_QUALITY, q};
        cv::imwrite(p, src, params);
        cv::Mat rec = cv::imread(p, cv::IMREAD_GRAYSCALE);
        double file_size = (double)std::filesystem::file_size(p) / 1024.0;
        jpeg_kb.push_back(file_size);
        jpeg_psnr.push_back(mm::psnr(src, rec));
    }

    std::vector<double> webp_kb, webp_psnr;
    std::vector<int> webp_qualities = {30, 50, 70, 85, 95};
    for (int q : webp_qualities) {
        std::string p = "tmp/fmt_w" + std::to_string(q) + ".webp";
        std::vector<int> params = {cv::IMWRITE_WEBP_QUALITY, q};
        cv::imwrite(p, src, params);
        cv::Mat rec = cv::imread(p, cv::IMREAD_GRAYSCALE);
        double file_size = (double)std::filesystem::file_size(p) / 1024.0;
        webp_kb.push_back(file_size);
        webp_psnr.push_back(mm::psnr(src, rec));
    }

    std::string p_png = "tmp/fmt.png";
    std::vector<int> png_params = {cv::IMWRITE_PNG_COMPRESSION, 9};
    cv::imwrite(p_png, src, png_params);
    double png_kb = (double)std::filesystem::file_size(p_png) / 1024.0;

    std::string title = "Curva Taxa-Distorcao (PNG sem perda: " + 
                        std::to_string(png_kb) + " KB)";

    // Converte para std::vector<std::vector<double>> como esperado pelo lineChart
    std::vector<std::vector<double>> xs = {jpeg_kb, webp_kb};
    std::vector<std::vector<double>> ys = {jpeg_psnr, webp_psnr};

    mm::Image chart = mm::lineChart(
        xs, ys,
        {"JPEG", "WebP"},
        {},  // colors vazio
        title,
        "Tamanho do arquivo (KB)", "PSNR (dB)"
    );

    std::vector<mm::Image> display = {chart};
    mm::show(display, MM_OUT, {"JPEG x WebP x PNG"}, 1);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(chart, "tmp/fig_05_formatos_comparacao_0.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_05_formatos_comparacao.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_formatos_comparacao.cpp -o tmp/fig_05_formatos_comparacao -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_formatos_comparacao \
  && test -f "tmp/fig_05_formatos_comparacao.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_formatos_comparacao.png"
[1] JPEG x WebP x PNG
try:
    mm.show(
        [
            mm.read("tmp/fig_05_formatos_comparacao_0.png"),
        ],
        titles=[
            'JPEG x WebP x PNG',
        ],
        cols=1,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_formatos_comparacao_0.png (ver a versao Python)")
Figura 5.28: Curva taxa-distorção: PSNR vs tamanho de arquivo para JPEG, WebP e PNG aplicada à imagem do Cameraman.
NotaTamanho original da imagem

A imagem Cameraman (\(256 \times 256\) pixels em escala de cinza) ocupa 64 KB em formato bruto (sem compressão). Como referência, o PNG lossless comprime esse volume para 36,2 KB — evidenciando que a compressão sem perdas já reduz significativamente o armazenamento para imagens com regiões homogêneas. Em contrapartida, os formatos com perda (JPEG e WebP) atingem tamanhos ainda menores: o JPEG com qualidade 95 ocupa 22,3 KB (PSNR ≈ 45 dB), enquanto o WebP com qualidade 90 atinge 12,5 KB com PSNR equivalente, demonstrando sua superioridade em eficiência de compressão.

5.8.4.2 Mapeamento Espacial de Erros e Correlação Perceptual

Embora o PSNR ofereça um indicativo numérico rápido, métricas globais falham em discriminar como a perda de informação se distribui geometricamente sobre a imagem. A Figura 5.29 soluciona essa limitação ao associar as reconstruções em diferentes qualidades aos seus respectivos mapas de erro absoluto e ao SSIM.

Os mapas residuais — obtidos pela diferença absoluta normalizada entre a imagem original e a comprimida — revelam a assinatura espacial intrínseca de cada arquitetura de codificação:

  • Em altas qualidades (\(Q=95\) a \(Q=75\)): As distorções concentram-se predominantemente ao redor de transições abruptas de intensidade (bordas), fruto do espelhamento espectral decorrente do descarte de altas frequências. O índice SSIM permanece próximo à unidade, atestando a integridade das estruturas originais.
  • Em qualidades agressivas (\(Q=50\) a \(Q=25\)): O erro assume uma estrutura de malha ortogonal regularizada. Esse padrão geométrico evidencia o surgimento dos artefatos de bloco (blocking artifacts), indicando que a quantização severa corrompeu a correlação espacial entre blocos adjacentes de \(8 \times 8\) pixels.

O SSIM captura essa degradação morfológica de forma muito mais sensível que o PSNR, penalizando o escore final à medida que a organização estrutural e as texturas finas — às quais o sistema visual humano é altamente responsivo — são eliminadas pelo codificador.

%%writefile tmp/fig_05_ssim_artefatos.cpp
#define MM_OUT "tmp/fig_05_ssim_artefatos.png"
//| label: fig-05-ssim-artefatos
//| fig-cap: "Análise espacial de degradação: imagens reconstruídas e respectivos mapas de erro absoluto normalizados para diferentes fatores de qualidade JPEG."
//| echo: true
//| output: true

#include <opencv2/opencv.hpp>
#include <cstdio>
#include <string>
#include <vector>
#include "morph.hpp"

// Implementação padrão de SSIM com janela gaussiana (Wang et al.)
static double compute_ssim(const cv::Mat& img1, const cv::Mat& img2) {
    const double C1 = 6.5025, C2 = 58.5225;
    cv::Mat I1, I2;
    img1.convertTo(I1, CV_64F);
    img2.convertTo(I2, CV_64F);

    cv::Mat mu1, mu2;
    cv::GaussianBlur(I1, mu1, cv::Size(11, 11), 1.5);
    cv::GaussianBlur(I2, mu2, cv::Size(11, 11), 1.5);

    cv::Mat mu1_sq = mu1.mul(mu1);
    cv::Mat mu2_sq = mu2.mul(mu2);
    cv::Mat mu1_mu2 = mu1.mul(mu2);

    cv::Mat sigma1_sq, sigma2_sq, sigma12;
    cv::GaussianBlur(I1.mul(I1), sigma1_sq, cv::Size(11, 11), 1.5);
    sigma1_sq -= mu1_sq;
    cv::GaussianBlur(I2.mul(I2), sigma2_sq, cv::Size(11, 11), 1.5);
    sigma2_sq -= mu2_sq;
    cv::GaussianBlur(I1.mul(I2), sigma12, cv::Size(11, 11), 1.5);
    sigma12 -= mu1_mu2;

    cv::Mat ssim_map;
    cv::Mat t1 = (2 * mu1_mu2 + C1).mul(2 * sigma12 + C2);
    cv::Mat t2 = (mu1_sq + mu2_sq + C1).mul(sigma1_sq + sigma2_sq + C2);
    cv::divide(t1, t2, ssim_map);
    return cv::mean(ssim_map)[0];
}

int main() {
    // Cameraman (asset do capítulo), 256x256 — mesma imagem da trilha py.
    cv::Mat img_gray_tmp = cv::imread("imagens/cameraman.png", cv::IMREAD_GRAYSCALE);
    cv::resize(img_gray_tmp, img_gray_tmp, cv::Size(256, 256));
    cv::Mat img_gray = img_gray_tmp; // mm::gray já é 1 canal; aqui já veio 1 canal

    std::vector<cv::Mat> imgs_ssim;
    std::vector<std::string> titles_ssim;
    imgs_ssim.push_back(img_gray);
    titles_ssim.push_back("Original");

    for (int q : {25, 50, 75, 95}) {
        cv::Mat rec = mm::jpegCompress(img_gray, q);  // DCT 8x8 -> quant -> IDCT
        double psnr_v = mm::psnr(img_gray, rec);
        double ssim_v = compute_ssim(img_gray, rec);

        cv::Mat diff64, diff_vis;
        cv::absdiff(img_gray, rec, diff64);
        diff64.convertTo(diff64, CV_64F);
        cv::normalize(diff64, diff_vis, 0, 255, cv::NORM_MINMAX, CV_8U);

        imgs_ssim.push_back(rec);
        imgs_ssim.push_back(diff_vis);

        char buf1[128], buf2[128];
        snprintf(buf1, sizeof(buf1), "Q=%d (PSNR=%.1fdB | SSIM=%.3f)", q, psnr_v, ssim_v);
        snprintf(buf2, sizeof(buf2), "Mapa de erro (Q=%d) - bordas e blocagem", q);
        titles_ssim.push_back(buf1);
        titles_ssim.push_back(buf2);
    }

    // Reconstrução dos mm::Image para exibição
    std::vector<mm::Image> out_imgs;
    for (auto& m : imgs_ssim) out_imgs.emplace_back(m);
    mm::show(out_imgs, MM_OUT, titles_ssim, 3);
    return 0;
}
Overwriting tmp/fig_05_ssim_artefatos.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_ssim_artefatos.cpp -o tmp/fig_05_ssim_artefatos -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_ssim_artefatos \
  && test -f "tmp/fig_05_ssim_artefatos.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_ssim_artefatos.png"
[1] Original
[2] Q=25 (PSNR=30.7dB | SSIM=0.860)
[3] Mapa de erro (Q=25) - bordas e blocagem
[4] Q=50 (PSNR=32.8dB | SSIM=0.905)
[5] Mapa de erro (Q=50) - bordas e blocagem
[6] Q=75 (PSNR=35.2dB | SSIM=0.938)
[7] Mapa de erro (Q=75) - bordas e blocagem
[8] Q=95 (PSNR=44.8dB | SSIM=0.990)
[9] Mapa de erro (Q=95) - bordas e blocagem
try:
    mm.show(mm.read("tmp/fig_05_ssim_artefatos.png"), figsize=(14, 14))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_ssim_artefatos.png (ver a versao Python)")
Figura 5.29: Análise espacial de degradação: imagens reconstruídas e respectivos mapas de erro absoluto normalizados para diferentes fatores de qualidade JPEG.
NotaInterpretando os mapas de erro

Os mapas de erro apresentados foram normalizados individualmente (cv2.NORM_MINMAX) para maximizar o contraste visual e revelar a estrutura espacial das distorções. Isso significa que:

  • Em Q=95, o erro absoluto é da ordem de 0.5–1.5 níveis de cinza (imperceptível visualmente), mas a normalização o amplifica para preto-branco para evidenciar sua localização em bordas e transições.
  • Em Q=25, o erro absoluto é 10–20 vezes maior (5–15 níveis de cinza), mas a normalização também o leva ao mesmo intervalo [0, 255].

Portanto, a intensidade do branco nos mapas NÃO é comparável entre diferentes qualidades — os mapas servem apenas para revelar a assinatura espacial do erro (bordas vs blocos), não sua magnitude. A magnitude correta é dada pelos valores de PSNR e SSIM, que mostram claramente que Q=95 tem erro muito menor que Q=25.

Síntese — Compressão JPEG

O processo de compressão no padrão JPEG baseia-se na aplicação combinada de transformações espaciais, perceptuais e estatísticas para reduzir as redundâncias de uma imagem. A Tabela 5.9 resume o papel de cada etapa no pipeline e seu respectivo impacto na redução de dados.

Tabela 5.9: Síntese das etapas do pipeline de compressão JPEG e seus respectivos impactos.
Etapa Operação Analítica Mecanismo de Ganho / Compressão
Conversão \(YC_bC_r\) Isolamento dos canais de luminância e crominância. Modela a percepção do SVH, permitindo tratar cor e brilho de forma independente.
Subamostragem 4:2:0 Redução da resolução espacial dos canais de cor (\(C_b\) e \(C_r\)). Elimina aproximadamente 50% dos dados brutos com impacto visual mínimo.
DCT \(8 \times 8\) Mapeamento do domínio espacial para o domínio de frequências espaciais. Compactação de energia, concentrando a informação vital nos primeiros coeficientes.
Quantização Linear Divisão inteira dos coeficientes por uma matriz de ponderação \(Q(u,v)\). Principal fonte de compressão com perda; elimina altas frequências imperceptíveis.
Codificação Entrópica Aplicação de algoritmos RLE e codificação de Huffman. Compressão estatística sem perda, otimizada pelas longas corridas de coeficientes nulos.

Artefatos de Degradação Característicos

A aplicação de taxas de compressão excessivamente agressivas (fatores de qualidade reduzidos) introduz distorções previsíveis na imagem reconstruída, decorrentes das limitações matemáticas do modelo:

  • Artefatos de bloco (blocking artifacts): Descontinuidades geométricas visíveis nas fronteiras dos blocos de \(8 \times 8\) pixels, causadas pela perda de correlação espacial após a quantização severa das componentes AC.
  • Efeito de espalhamento (ringing): Oscilações fantasmas ou distorções de “fumaça” ao redor de bordas nítidas e de alto contraste, provocadas pela eliminação abrupta de harmônicos de alta frequência necessários para reconstruir funções degrau.
  • Perda de textura fina: Atenuação de detalhes de alta frequência e baixo contraste (como gramados, tecidos ou porosidade), fazendo com que regiões originalmente texturizadas assumam um aspecto excessivamente liso ou homogeneizado.

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

Reunindo as técnicas consolidadas ao longo deste capítulo, apresenta-se um pipeline completo de restauração de imagens que combina a análise espectral no domínio da frequência com a filtragem adaptativa no domínio espacial. O objetivo é atenuar um ruído misto (composto por degradação Gaussiana e interferência periódica) preservando ao máximo os detalhes estruturais da imagem original.

\[ \text{Imagem Ruidosa} \xrightarrow{\text{FFT2}} \xrightarrow{\text{Filtro Notch Gaussiano}} \xrightarrow{\text{IFFT2}} \xrightarrow{\text{Filtro Bilateral}} \text{Imagem Restaurada} \]

NotaAvaliação Complementar: PSNR vs. SSIM

O par de métricas estatísticas PSNR e SSIM fornece uma avaliação qualitativa e morfológica complementar do processo de restauração:

  • PSNR: Penaliza uniformemente o desvio quadrático médio pixel a pixel.
  • SSIM: Avalia a preservação de estruturas locais perceptualmente relevantes (luminância, contraste e contornos).

Na prática, existe um compromisso analítico (trade-off) entre redução de ruído e preservação de detalhes: filtros espaciais excessivamente agressivos atenuam bem o ruído de alta frequência, mas degradam texturas finas e suavizam bordas nítidas — o que reduz simultaneamente tanto o PSNR quanto o SSIM em relação à imagem original. O desafio do projeto de filtros é encontrar o ponto de equilíbrio que maximize ambas as métricas, garantindo uma restauração fiel e visualmente agradável.

5.9.1 Análise de Desempenho e Conclusão do Capítulo

Os resultados numéricos e visuais gerados pela Figura 5.30 demonstram a relevância prática de associar diferentes domínios de processamento. A inserção simultânea de ruído periódico e estocástico corrompe as propriedades morfológicas do sinal, reduzindo severamente os índices de similaridade e a relação sinal-ruído da imagem de referência.

O isolamento e a supressão dos picos harmônicos no domínio da frequência por meio da máscara notch removem as franjas de interferência senoidais espalhadas sobre o espaço bi-dimensional. Como evidenciado nos dados impressos da Figura 5.30, essa filtragem cirúrgica promove um salto imediato e substancial na métrica PSNR. Contudo, o ruído Gaussiano de alta frequência permanece ativo de forma homogênea no espectro, exigindo uma abordagem complementar.

A restauração final é consolidada no domínio espacial com a introdução do filtro bilateral. Diferentemente de operadores passa-baixas convencionais (como o Gaussiano ou de média), que suavizariam indiscriminadamente o ruído e os contornos estruturais, a filtragem bilateral calcula pesos ponderados pela proximidade geométrica e pela diferença de intensidade radiométrica. Esse comportamento adaptativo atenua as flutuações estocásticas remanescentes nas regiões de transição suave e preserva a nitidez das bordas espaciais.

A convergência de ambas as abordagens resulta em uma melhoria substancial e simultânea do PSNR e do SSIM em relação à imagem ruidosa — embora os valores finais permaneçam inferiores aos da imagem original (PSNR = \(\infty\), SSIM = 1,0), devido à perda inevitável de informações espectrais e texturais durante os processos de filtragem. A atenuação suave (gaussiana) dos picos no espectro evita artefatos de ringing, enquanto o filtro bilateral elimina o ruído estocástico residual sem comprometer a nitidez das bordas. Os resultados comprovam a eficácia e a complementaridade prática das ferramentas de análise de frequência apresentadas neste capítulo, demonstrando que a filtragem híbrida (frequência + espacial) é superior a qualquer abordagem isolada para a restauração de imagens degradadas por ruído misto.

%%writefile tmp/fig_05_pipeline_denoising.cpp
#define MM_OUT "tmp/fig_05_pipeline_denoising.png"
//| label: fig-05-pipeline-denoising
//| fig-cap: "*Pipeline* completo de remoção de ruído misto: (1) adição de ruído gaussiano e periódico; (2) identificação de picos de interferência no espectro de frequências; (3) aplicação de máscara *notch* com atenuação gaussiana suave; (4) pós-processamento via filtro bilateral para eliminação do ruído estocástico residual."
//| echo: true
//| output: true

#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <cmath>
#include <random>
#include <iostream>
#include "morph.hpp"

int main() {
    // Cameraman (asset do capítulo), 256x256 — mesma imagem da trilha py.
    mm::Image img_gray = mm::gray(mm::read("imagens/cameraman.png"));
    cv::Mat img_gray_cv = img_gray;
    cv::resize(img_gray_cv, img_gray_cv, cv::Size(256, 256));
    img_gray = mm::Image(img_gray_cv);
    int h_img = img_gray.h;
    int w_img = img_gray.w;

    // ── 1. Ruído misto: gaussiano + periódico ───────────────────────────────────
    std::mt19937 gen(42);
    std::normal_distribution<double> dist(0.0, 15.0);

    cv::Mat ruido_gauss(h_img, w_img, CV_64F);
    for (int y = 0; y < h_img; y++) {
        for (int x = 0; x < w_img; x++) {
            ruido_gauss.at<double>(y, x) = dist(gen);
        }
    }

    double u0 = 15.0, v0 = 10.0;
    cv::Mat ruido_period(h_img, w_img, CV_64F);
    for (int y = 0; y < h_img; y++) {
        for (int x = 0; x < w_img; x++) {
            ruido_period.at<double>(y, x) = 30.0 * std::sin(2.0 * M_PI * (u0 * x / w_img + v0 * y / h_img));
        }
    }

    cv::Mat img_gray_float;
    img_gray_cv.convertTo(img_gray_float, CV_64F);
    cv::Mat img_noisy_float = img_gray_float + ruido_gauss + ruido_period;
    cv::Mat img_noisy_clipped;
    cv::threshold(img_noisy_float, img_noisy_clipped, 0, 255, cv::THRESH_TOZERO);
    cv::threshold(img_noisy_clipped, img_noisy_clipped, 255, 255, cv::THRESH_TRUNC);
    cv::Mat img_noisy_8u;
    img_noisy_clipped.convertTo(img_noisy_8u, CV_8U);
    mm::Image img_noisy(img_noisy_8u);

    // ── 2. Espectro (log-magnitude) da imagem ruidosa ──────────────────────────
    mm::Image mag_n = mm::spectrumMag(img_noisy);

    // ── 3. Máscara notch gaussiana nos 4 picos periódicos ──────────────────────
    cv::Mat mascara_notch = cv::Mat::ones(h_img, w_img, CV_64F);
    int cy0 = h_img / 2, cx0 = w_img / 2;

    auto suprimir_pico_gaussiano = [&](cv::Mat& mask, int cy, int cx, double sigma) {
        for (int y = 0; y < h_img; y++) {
            for (int x = 0; x < w_img; x++) {
                double dx = x - cx;
                double dy = y - cy;
                double notch = std::exp(-(dy * dy + dx * dx) / (2.0 * sigma * sigma));
                mask.at<double>(y, x) *= (1.0 - notch);
            }
        }
    };

    int dys[] = {(int)v0, -(int)v0, (int)v0, -(int)v0};
    int dxs[] = {(int)u0, -(int)u0, -(int)u0, (int)u0};
    for (int i = 0; i < 4; i++) {
        suprimir_pico_gaussiano(mascara_notch, cy0 + dys[i], cx0 + dxs[i], 3.0);
    }

    // ── 4. Filtragem: notch no domínio da frequência + bilateral ───────────────
    mm::Image img_notch = mm::freqFilter(img_noisy, mascara_notch);

    cv::Mat img_notch_cv = img_notch;
    cv::Mat img_den_cv;
    cv::bilateralFilter(img_notch_cv, img_den_cv, 7, 25, 7);
    mm::Image img_den(img_den_cv);

    cv::Mat mascara_vis_float = mascara_notch * 255.0;
    cv::Mat mascara_vis_8u;
    mascara_vis_float.convertTo(mascara_vis_8u, CV_8U);
    mm::Image mascara_vis(mascara_vis_8u);

    double psnr_n = mm::psnr(img_gray, img_noisy);
    double psnr_no = mm::psnr(img_gray, img_notch);
    double psnr_d = mm::psnr(img_gray, img_den);
    char buffer[200];
    snprintf(buffer, sizeof(buffer), "Ruidosa: PSNR=%.2f dB | Apos notch: %.2f dB | Notch+bilateral: %.2f dB", psnr_n, psnr_no, psnr_d);
    std::cout << buffer << std::endl;

    // Preparar títulos com PSNR
    char titulo_ruidosa[100], titulo_notch[100], titulo_den[100];
    snprintf(titulo_ruidosa, sizeof(titulo_ruidosa), "Ruidosa (PSNR=%.1f dB)", psnr_n);
    snprintf(titulo_notch, sizeof(titulo_notch), "Apos notch (PSNR=%.1f dB)", psnr_no);
    snprintf(titulo_den, sizeof(titulo_den), "Notch + bilateral (PSNR=%.1f dB)", psnr_d);

    std::vector<mm::Image> imagens = {img_gray, img_noisy, mag_n, mascara_vis, img_notch, img_den};
    std::vector<std::string> titulos = {
        "Original",
        titulo_ruidosa,
        "Espectro (picos visiveis)",
        "Mascara notch (gaussiana)",
        titulo_notch,
        titulo_den
    };

    mm::show(imagens, MM_OUT, titulos, 6);

    return 0;
}
Overwriting tmp/fig_05_pipeline_denoising.cpp
!g++ -I. -std=c++17 -DMM_USE_OPENCV -I/usr/include/opencv4 tmp/fig_05_pipeline_denoising.cpp -o tmp/fig_05_pipeline_denoising -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core \
  && ./tmp/fig_05_pipeline_denoising \
  && test -f "tmp/fig_05_pipeline_denoising.png" \
  || echo "⚠ mm::show não gravou tmp/fig_05_pipeline_denoising.png"
Ruidosa: PSNR=20.20 dB | Apos notch: 22.36 dB | Notch+bilateral: 23.54 dB
[1] Original
[2] Ruidosa (PSNR=20.2 dB)
[3] Espectro (picos visiveis)
[4] Mascara notch (gaussiana)
[5] Apos notch (PSNR=22.4 dB)
[6] Notch + bilateral (PSNR=23.5 dB)
try:
    mm.show(mm.read("tmp/fig_05_pipeline_denoising.png"), figsize=(20, 4))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_05_pipeline_denoising.png (ver a versao Python)")
Figura 5.30: Pipeline completo de remoção de ruído misto: (1) adição de ruído gaussiano e periódico; (2) identificação de picos de interferência no espectro de frequências; (3) aplicação de máscara notch com atenuação gaussiana suave; (4) pós-processamento via filtro bilateral para eliminação do ruído estocástico residual.

5.10 Resumo do Capítulo

A transição do domínio espacial para o domínio da frequência revela a distribuição espectral de energia da imagem, estabelecendo a base analítica para a filtragem avançada, restauração e compressão de dados. A articulação estrutural desses conceitos é sintetizada no mapa conceitual da Figura 5.31.

Figura 5.31: Mapa conceitual das transformações e propriedades no domínio da frequência.

Fundamentos Essenciais

  • DFT e Percepção Visual: O espectro decompõe a imagem em componentes harmônicas. A fase retém a inteligibilidade geométrica da cena e a localização de contornos, enquanto a magnitude dita a distribuição de contraste e amplitudes globais.
  • Eficiência Algorítmica: O Teorema da Convolução viabiliza o processamento de máscaras de grande escala no domínio da frequência via FFT, reduzindo a complexidade computacional assintótica de \(O(N^2 K^2)\) no espaço para \(O(N^2 \log N)\).
  • Fenômeno de Ringing: Cortes abruptos no espectro (Filtros Ideais) geram oscilações espaciais indesejadas (fenômeno de Gibbs). A atenuação suave por filtros de Butterworth ou Gaussianos elimina essas descontinuidades.
  • Análise Multirresolução via Wavelets: Superando o caráter puramente global de Fourier, a DWT captura a frequência e a localização espacial simultaneamente, fundamentando o padrão JPEG 2000 e subsidiando representações hierárquicas análogas às extrações de feições em Redes Neurais Convolucionais (CNNs).
  • Compressão Perceptual (DCT): O pipeline JPEG explora as limitações de contraste do sistema visual humano em altas frequências espaciais. A DCT isola a energia de blocos \(8 \times 8\), permitindo que a quantização descarte coeficientes AC de detalhes finos sem prejuízo perceptual severo.

Próximos Passos: O Capítulo 6 inaugura a Parte II da obra, aplicando as ferramentas de processamento de imagens na resolução de problemas reais de inspeção industrial. Serão exploradas técnicas de segmentação e análise de formas para a detecção automática de falhas em linhas de produção — desde a identificação de defeitos superficiais em peças até a leitura QRCode em provas, consolidando a ponte entre a teoria apresentada na Parte I e as demandas práticas da visão computacional.

5.11 🤖 Uso do Gemini Notebook como Tutor Complementar

Nesta edição, incentiva-se o uso da plataforma Gemini Notebook como ferramenta complementar de aprendizagem — não como substituta da leitura atenta, da resolução de exercícios ou da experimentação prática. Baseado em arquiteturas de inteligência artificial, o sistema utiliza exclusivamente o material didático e os documentos fornecidos pelo autor como base de conhecimento, assegurando que as respostas geradas estejam conceitualmente alinhadas ao conteúdo programático e à abordagem pedagógica adotada ao longo desta obra.

ImportanteAcesso ao Tutor Inteligente

🚀 ACESSAR Gemini Notebook: CAPÍTULO 05

🌐 Idioma e Linguagem de Programação

O projeto deste capítulo no Gemini Notebook foi construído apenas com o texto em português e os exemplos de código em Python. Se você está estudando pela edição em inglês ou francês, ou acompanhando a trilha em C++, as respostas do tutor podem não corresponder exatamente à versão que você está lendo.

Diretrizes sobre o Conteúdo Gerado por Inteligência Artificial

Embora as ferramentas de inteligência artificial constituam aliados eficientes no processo de aprendizagem e revisão, o conteúdo gerado está sujeito a inconsistências ou imprecisões técnicas. Desse modo, é indispensável a consulta sistemática a livros-texto, artigos científicos e fontes acadêmicas indexadas para a validação rigorosa das informações. Recomenda-se veementemente a execução e a modificação dos exemplos práticos em Python fornecidos neste capítulo como método primário de verificação experimental dos resultados.

5.12 Lista de Exercícios

  1. (10%) Implementação Direta da DFT 2D: Implemente analiticamente a Transformada Discreta de Fourier 2D (DFT) sem o auxílio de funções nativas de bibliotecas (como np.fft.fft2), utilizando estritamente a formulação matemática definida na Equação 5.1 para uma matriz de dimensões \(16 \times 16\). Realize a validação numérica comparando os coeficientes gerados com os resultados da função np.fft.fft2, certificando-se de que o desvio absoluto máximo seja inferior a \(10^{-8}\). Mensure os tempos de execução de ambos os métodos e apresente uma justificativa teórica para a disparidade observada em termos de complexidade assintótica.

  2. (15%) Supressão de Ruído Periódico: Adicione interferências senoidais com frequências espaciais \((u_0, v_0) \in \{(5,10), (20,5), (30,30)\}\) à imagem de teste do Cameraman. Para cada cenário de degradação, projete uma máscara de filtragem notch específica no domínio da frequência para isolar e atenuar os picos harmônicos indesejados. Avalie quantitativamente a eficácia do processo de restauração por meio do cálculo das métricas de PSNR e SSIM. Discuta analiticamente o compromisso (trade-off) entre a atenuação do ruído senoidal e a indesejada atenuação de feições estruturais legítimas da imagem.

  3. (15%) Análise Comparativa de Operadores Passa-Baixa: Realize um estudo comparativo entre os filtros passa-baixa Ideal, Gaussiano e Butterworth (com ordens harmônicas \(n = 1, 2, 4\)), parametrizados com frequências de corte \(D_0 = 20, 40, 60\) pixels. Para cada combinação estrutural, calcule os índices PSNR e SSIM da imagem resultante frente ao sinal original de referência. Organize os dados quantitativos em uma tabela estruturada e plote os gráficos unidimensionais das funções de transferência correspondentes ao longo do perfil horizontal \(H(u, 0)\).

  4. (15%) Banco de Filtros Multirresolução de Haar: Desenvolva um script para executar manualmente a decomposição wavelet discreta 2D de primeiro nível utilizando a família Haar. O algoritmo deve calcular os coeficientes dos filtros correspondentes passa-baixa (\(h\)) e passa-alta (\(g\)), aplicando-os de forma separável sobre as linhas e colunas da matriz, seguidos pela operação de decimação (subamostragem espacial por um fator de 2). Valide numericamente a exatidão da sua implementação confrontando as subbandas obtidas com a saída da função pywt.dwt2(img, 'haar').

  5. (15%) Compressão Esparsa por Limiarização Wavelet: Aplique a técnica de filtragem por limiarização abrupta (hard thresholding) sobre os coeficientes de detalhe da decomposição wavelet, adotando os limiares numéricos \(T \in \{5, 10, 20, 40, 80\}\) para as famílias Haar, Daubechies (db4) e Symlets (sym4). Após realizar o processo de síntese por meio da transformada inversa (pywt.waverec2), compute os valores de PSNR e SSIM de cada imagem reconstruída. Identifique e justifique qual combinação de família wavelet e limiar \(T\) maximiza a similaridade estrutural.

  6. (15%) Construção de Codificador JPEG Simplificado: Implemente o pipeline completo de compressão de dados simulando o padrão JPEG. O fluxo deve englobar: conversão espacial \(RGB \rightarrow YC_bC_r\), subamostragem cromática na proporção 4:2:0, segmentação da luminância em blocos disjuntos de \(8 \times 8\) pixels, aplicação da DCT-II 2D ortogonal, e quantização linear baseada na matriz normalizada de luminância escalada por fatores de qualidade desejados. Realize a decodificação inversa e compare quantitativamente as reconstruções com os arquivos gerados pela função cv2.imencode para os fatores de qualidade de 20, 50 e 80.

  7. (15%) Análise Perceptual em Conteúdos Heterogêneos: Desenvolva uma imagem sintética composta por três regiões distintas e de características espectrais contrastantes: uma textura fotográfica complexa (representando altas frequências estocásticas), uma área de texto vetorizado com bordas nítidas (representando transições degrau puras) e um gradiente linear contínuo (representando baixas frequências homogêneas). Submeta essa imagem mista aos processos de compressão sob os formatos JPEG, PNG e WebP. Avalie e interprete os resultados correlacionando o tamanho final do arquivo em disco às métricas PSNR e SSIM obtidas, justificando qual formato exibe o melhor desempenho para sinais de natureza heterogênea e por que essa vantagem ocorre em termos de compactação de energia e preservação perceptual.

Referências do Capítulo

A fundamentação teórica e o desenvolvimento analítico dos conceitos abordados neste capítulo fundamentam-se nas seguintes obras de referência:

  • Gonzalez (2018) — Formulações clássicas de Transformadas Discretas de Fourier 2D (DFT), projeto de filtros analíticos no domínio da frequência, Transformada Discreta de Cossenos (DCT) e princípios fundamentais de sistemas de compressão de imagens.
  • Oppenheim (2010) — Teoria formal de sinais e sistemas aplicados no domínio discreto, cobrindo as propriedades matemáticas da DFT e a modelagem analítica do Teorema da Convolução.
  • Mallat (1999) — Fundamentação matemática da teoria de wavelets, formalização da análise multirresolução (MRA) e arquitetura de bancos de filtros diádicos.
  • Wallace (1991) — Especificação original e aspectos de engenharia do padrão de compressão ISO/IEC JPEG, com ênfase nos critérios psicovisuais para o projeto de matrizes de quantização DCT.
  • Szeliski (2022) — Modelagem computacional e caracterização de métricas modernas de fidelidade e qualidade perceptual (PSNR e SSIM), bem como a análise comparativa de formatos de imagem rasterizados de alto desempenho.

5.13 💻 Parte Prática com Exercícios de Programação

A presente lista de exercícios de programação (EP) consolida as formulações teóricas apresentadas ao longo do Capítulo 5 — Transformadas e Compressão — por meio de uma trilha prática aplicada. Os exercícios são estruturados a partir de matrizes de dimensões reduzidas, viabilizando a validação analítica e a inspeção manual de cada coeficiente, mantendo a consistência metodológica adotada nos capítulos anteriores.

O encadeamento dos exercícios reproduz rigorosamente o fluxo conceitual do capítulo: inicia-se com a implementação explícita da Transformada Discreta de Fourier (DFT) a partir de sua definição matemática fundamental; avança-se para o projeto de filtros passa-baixa e máscaras notch no domínio da frequência; aplica-se a quantização de coeficientes (núcleo da compressão com perda); e conclui-se com a integração dessas etapas na construção de um pipeline de compressão JPEG simplificado e na análise perceptual de formatos de imagem.

ImportanteDiretrizes para a Resolução dos Exercícios de Programação

Em todos os exercícios deste capítulo, as coordenadas do centro do espectro (origem das frequências espaciais pós-aplicação do deslocamento fftshift) devem ser determinadas via divisão inteira. Para uma matriz com \(L\) linhas e \(C\) colunas, a componente de frequência nula localiza-se na posição:

\[ (c_y, c_x) = \left( \left\lfloor \frac{L}{2} \right\rfloor, \left\lfloor \frac{C}{2} \right\rfloor \right) \]

Esta convenção é rigorosamente idêntica à adotada pela função np.fft.fftshift. Ademais, em todas as etapas que exijam discretização ou arredondamento numérico (seja na quantização de coeficientes AC ou na reconstrução final de pixels), deve-se empregar o arredondamento padrão para o inteiro mais próximo (round half away from zero), mitigando ambiguidades em valores com fração exatamente igual a \(0.5\).

🎯 Objetivo deste Caderno

O caderno permite desenvolver, validar, organizar e testar soluções de Exercícios de Programação (EPs) em ambientes interativos, como o Colab, com os mesmos casos de teste do Moodle, copiando para lá apenas na hora de registrar a nota oficial.

Download

Baixe morph.py e testsuite.py executando a célula abaixo:

import os, urllib.request

os.makedirs("tmp/state", exist_ok=True)

url = "https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph/config.py"
if not os.path.exists("config.py"):
    urllib.request.urlretrieve(url, "config.py")

import config
config.setup(testsuite=True, cpp=True)
from morph import mm
from testsuite import TestSuite
✅ Ambiente pronto. Morph: 1.1.9 | OpenCV: 5.0.0 | TestSuite: 1.1.2

Executando os Testes

Para avaliar os testes, execute TestSuite("EP05_01.extensão").run() numa nova célula, trocando a extensão pela da linguagem usada (.py, .java, .c, .cpp, .js ou .r). O sistema baixa os casos de teste do GitHub, executa o programa e calcula a nota automaticamente.

Para testar código Python diretamente, sem salvar arquivo, use run_code(codigo) passando o código como string numa variável codigo:

codigo = """
from morph import mm
# ... seu código aqui ...
"""
TestSuite("EP05_01").run_code(codigo)

5.13.1 EP05_01 🟢 Filtro Passa-Baixa Ideal por Distância no Espectro

Em um scanner de documentos antigo, o sensor capta papel amassado e textura de fibra junto com o texto — ruído de alta frequência que “polui” o espectro nas bordas. O técnico de manutenção não tem acesso à imagem original, apenas ao espectro de magnitude já calculado pelo software do scanner. Seu trabalho é simples e cirúrgico: manter apenas o círculo central de baixas frequências (a estrutura global do documento) e apagar tudo que estiver fora do raio \(D_0\), eliminando a textura fina sem nem precisar tocar na imagem espacial.

Este é o Filtro Passa-Baixa Ideal (LPFI): a operação espectral mais direta do capítulo, mas também a que melhor revela a anatomia de um espectro centrado.

5.13.1.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas) do espectro de magnitude — já fornecido centrado (equivalente à saída de np.fft.fftshift).
  2. Frequência de corte: Ler o inteiro \(D_0\).
  3. Dados: Ler os valores inteiros da matriz de magnitude, linha a linha.
  4. Centro do espectro: Calcular \((c_y, c_x) = (L \mathbin{//} 2,\; C \mathbin{//} 2)\).
  5. Distância: Para cada posição \((u,v)\), calcular \[ D(u,v) = \sqrt{(u-c_y)^2 + (v-c_x)^2} \]
  6. Máscara ideal: Aplicar \[ H(u,v) = \begin{cases} 1, & D(u,v) \le D_0 \\ 0, & D(u,v) > D_0 \end{cases} \]
  7. Filtragem: O valor de saída é \(\text{mag}'(u,v) = \text{mag}(u,v) \cdot H(u,v)\).
  8. Saída: Exibir a matriz filtrada com dimensões \(L \times C\).

5.13.1.2 📌 Restrições Computacionais

  • Comparação não estrita: o critério usa \(D(u,v) \le D_0\) (a fronteira pertence ao filtro, ou seja, é mantida).
  • Tipo: todos os valores de entrada e saída são inteiros; a distância é calculada em ponto flutuante apenas internamente.
  • Sem arredondamento de magnitude: como a entrada já é inteira e a máscara é binária (0 ou 1), a saída nunca precisa de arredondamento.

5.13.1.3 🧠 Fundamentação Teórica

Região Distância ao centro Efeito do filtro
Centro (\(D \le D_0\)) Baixas frequências Preservadas — estrutura global mantida
Bordas (\(D > D_0\)) Altas frequências Zeradas — textura e ruído removidos
\(D_0\) pequeno — Imagem reconstruída ficaria muito borrada
\(D_0\) grande — Pouca filtragem; quase toda energia preservada

5.13.1.4 📦 Especificação de Entrada e Saída (VPL)

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linha 3: Inteiro \(D_0\).
  • Linhas seguintes: Elementos inteiros da matriz de magnitude (centrada).

Saída:

  • Matriz filtrada em \(L\) linhas e \(C\) colunas, separados por espaço.

5.13.1.5 📌 Exemplos

Entrada Saída Observação
3
3
1
10 20 30
40 50 60
70 80 90
0 20 0
40 50 60
0 80 0
Centro \((1,1)\). Cantos têm \(D=\sqrt{2}\approx1.41 > 1\), logo são zerados; vizinhos ortogonais têm \(D=1 \le 1\) e são mantidos.
1
3
0
5 9 7
0 9 0 \(L=1, C=3\): centro em \((0,1)\). Apenas a própria posição central (\(D=0\)) sobrevive a \(D_0=0\).
🎮 Simulador EP05_01: Filtro Passa-Baixa Ideal H = (D ≤ D₀) ? 1 : 0
Ajuste D₀ e observe quais posições do espectro 5×5 sobrevivem ao filtro.
Espectro Original (Magnitude)
Resultado Filtrado
–
Figura 5.32: Simulador EP05_01: Filtro Passa-Baixa Ideal no Espectro
%%writefile EP05_01.cpp
// sua solução
Overwriting EP05_01.cpp
TestSuite("EP05_01.cpp").run()
✔️ EP05_01.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP05_01.cases

🔍 Testando C++: EP05_01.cpp
⚠️ EP05_01.cpp: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

5.13.2 EP05_02 🟡 Filtro Notch: Removendo Picos Periódicos

Uma câmera de inspeção industrial captura imagens de placas de circuito, mas a fonte de alimentação da linha de produção introduz uma interferência elétrica periódica — um padrão de listras quase imperceptível a olho nu, mas que aparece no espectro de Fourier como pares de picos brilhantes simetricamente posicionados em torno do centro. A equipe de visão computacional não pode reprocessar a captura: precisa localizar e apagar cirurgicamente esses pares de picos no espectro, preservando todo o resto da informação útil da imagem.

Esse é o papel do filtro rejeita-banda notch: diferente do passa-baixa (que afeta uma região contínua), ele ataca pontos específicos e seus simétricos, deixando o restante do espectro intocado.

5.13.2.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas) do espectro de magnitude centrado.
  2. Dados: Ler os valores inteiros da matriz de magnitude, linha a linha.
  3. Picos: Ler o inteiro \(K\) (quantidade de pares de picos a remover).
  4. Para cada um dos \(K\) picos: ler três inteiros \(\Delta v\), \(\Delta u\), \(r\) — deslocamento vertical, deslocamento horizontal e raio do notch.
  5. Centro do espectro: \((c_y, c_x) = (L \mathbin{//} 2,\; C \mathbin{//} 2)\).
  6. Supressão simétrica: para cada pico, zerar todas as posições \((u,v)\) tais que a distância ao ponto \((c_y+\Delta v,\, c_x+\Delta u)\) for \(\le r\), e também todas as posições com distância \(\le r\) ao ponto simétrico \((c_y-\Delta v,\, c_x-\Delta u)\).
  7. Saída: Exibir a matriz resultante com dimensões \(L \times C\).

5.13.2.2 📌 Restrições Computacionais

  • Simetria obrigatória: cada pico informado gera dois discos zerados (o ponto e seu simétrico em relação ao centro) — esquecer o simétrico é o erro mais comum.
  • Sobreposição: se dois discos se sobrepõem, a posição permanece zerada (não há “soma” ou restauração).
  • Comparação não estrita: uma posição é zerada se \(\text{distância} \le r\).
  • Ordem de leitura: os \(K\) picos devem ser processados na ordem em que aparecem na entrada, mas o resultado final independe da ordem (operações de zerar são comutativas).

5.13.2.3 🧠 Fundamentação Teórica

Conceito Papel no filtro notch
Pico em \((\Delta v, \Delta u)\) Frequência da interferência periódica detectada visualmente no espectro
Ponto simétrico \((-\Delta v,-\Delta u)\) Toda DFT de sinal real é hermitiana: picos sempre aparecem em pares simétricos ao centro
Raio \(r\) Controla a “largura” da rejeição — \(r\) grande remove mais energia ao redor do pico, mas também informação útil

5.13.2.4 📦 Especificação de Entrada e Saída (VPL)

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linhas seguintes: Elementos inteiros da matriz de magnitude (centrada), \(L\) linhas.
  • Próxima linha: Inteiro \(K\).
  • \(K\) linhas seguintes: três inteiros \(\Delta v\), \(\Delta u\), \(r\) (separados por espaço).

Saída:

  • Matriz resultante em \(L\) linhas e \(C\) colunas, separados por espaço.

5.13.2.5 📌 Exemplos

Entrada Saída Observação
5
5
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
1
1 1 0
1 2 3 4 5
6 0 8 9 10
11 12 13 14 15
16 17 18 0 20
21 22 23 24 25
Centro \((c_y, c_x) = (2, 2)\). Pico informado \((\Delta v, \Delta u) = (1, 1)\) gera o ponto \((3, 3)\) (valor 19) e seu simétrico \((1, 1)\) (valor 7), ambos zerados com \(r=0\) (apenas os pontos exatos).
🎮 Simulador EP05_02: Filtro Notch Par Simétrico
1
1
0
Mova Δv e Δu para escolher o pico — observe que o par simétrico também é filtrado.
Espectro 5×5 (Vermelho = Removido pelo Filtro)
–
Figura 5.33: Simulador EP05_02: Filtro Notch
%%writefile EP05_02.cpp
// sua solução
Overwriting EP05_02.cpp
TestSuite("EP05_02.cpp").run()
✔️ EP05_02.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP05_02.cases

🔍 Testando C++: EP05_02.cpp
⚠️ EP05_02.cpp: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

5.13.3 EP05_03 🟠 Quantização DCT: a Verdadeira Fonte de Compressão

Um aplicativo de galeria de fotos precisa reduzir o tamanho de milhares de imagens antes de fazer upload para a nuvem, sem recodificar tudo do zero. O engenheiro responsável já tem os coeficientes DCT de cada bloco \(4\times4\) calculados (a etapa cara computacionalmente já foi feita) — falta apenas aplicar a tabela de quantização, a etapa que realmente descarta informação e gera compressão. Coeficientes de alta frequência, menos perceptíveis ao olho humano, recebem divisores grandes e tendem a virar zero; coeficientes de baixa frequência, mais perceptíveis, recebem divisores pequenos e sobrevivem quase intactos.

Você vai implementar exatamente essa etapa: quantizar e desquantizar (dividir, arredondar, multiplicar de volta) — o coração da compressão lossy do JPEG.

5.13.3.1 📋 Diretrizes de Implementação

  1. Dimensão do bloco: Ler o inteiro \(N\) (bloco \(N \times N\)).
  2. Coeficientes: Ler a matriz \(C\) de coeficientes DCT, \(N\) linhas com \(N\) inteiros cada (podem ser negativos).
  3. Tabela de quantização: Ler a matriz \(Q\), \(N\) linhas com \(N\) inteiros positivos cada.
  4. Quantização: Para cada posição \((u,v)\), calcular o índice quantizado \[ \tilde{C}(u,v) = \text{round}\!\left(\frac{C(u,v)}{Q(u,v)}\right) \] usando arredondamento padrão para o inteiro mais próximo (valores intermediários .5 nunca ocorrem nos casos de teste).
  5. Desquantização (reconstrução): Calcular \[ C'(u,v) = \tilde{C}(u,v) \times Q(u,v) \]
  6. Saída: Exibir a matriz reconstruída \(C'\), \(N \times N\), inteiros.

5.13.3.2 📌 Restrições Computacionais

  • Round-trip completo: a saída é o coeficiente reconstruído (\(\tilde{C} \times Q\)), não o índice quantizado isolado.
  • Divisão em ponto flutuante: a divisão \(C(u,v)/Q(u,v)\) deve ser feita em ponto flutuante antes do arredondamento — divisão inteira truncada produzirá resultado incorreto.
  • Sinal preservado: coeficientes negativos mantêm o sinal após quantização e reconstrução.
  • \(Q(u,v) > 0\) sempre: não há necessidade de tratar divisão por zero.

5.13.3.3 🧠 Fundamentação Teórica

Coeficiente Frequência Valor típico de \(Q\) Efeito da quantização
\(C(0,0)\) DC (média do bloco) Pequeno Quase sempre sobrevive — domina a energia
\(C(u,v)\) baixo \(u+v\) Baixa frequência Pequeno/médio Parcialmente preservado
\(C(u,v)\) alto \(u+v\) Alta frequência Grande Frequentemente vira zero — fonte da compressão

5.13.3.4 📦 Especificação de Entrada e Saída (VPL)

Entrada:

  • Linha 1: Inteiro \(N\).
  • \(N\) linhas seguintes: matriz \(C\) (coeficientes DCT, inteiros, podem ser negativos).
  • \(N\) linhas seguintes: matriz \(Q\) (tabela de quantização, inteiros positivos).

Saída:

  • Matriz reconstruída \(C'\), \(N \times N\), inteiros separados por espaço.

5.13.3.5 📌 Exemplos

Entrada Saída Observação
4
50 10 -5 0
8 -3 2 1
0 1 0 0
2 0 0 -1
2 5 7 8
4 7 8 11
6 8 11 12
9 11 12 14
50 10 -7 0
8 0 0 0
0 0 0 0
0 0 0 0
\(C(0,0)=50/2=25 \to 25\times2=50\) (preservado). \(C(0,2)=-5/7\approx-0.71\to-1\to-1\times7=-7\). Já \(C(1,1)=-3/7\approx-0.43\to0\): zerado pela quantização — a maior parte do bloco vira zero, ilustrando a compactação de energia no canto superior esquerdo.
🎮 Simulador EP05_03: Quantização DCT round(C / Q) × Q
Ajuste a escala de Q e veja quantos coeficientes sobrevivem (não-zero) após o round-trip.
Coeficientes DCT (C)
Reconstruído (round(C / Q) · Q)
–
Figura 5.34: Simulador EP05_03: Quantização DCT (round-trip)
%%writefile EP05_03.cpp
// sua solução
Overwriting EP05_03.cpp
TestSuite("EP05_03.cpp").run()
✔️ EP05_03.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP05_03.cases

🔍 Testando C++: EP05_03.cpp
⚠️ EP05_03.cpp: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

5.13.4 EP05_04 🔴 Implementando a DFT 2D a Partir da Definição

Um laboratório de pesquisa em astronomia computacional recebeu, de uma missão antiga, um pequeno sensor experimental cujos dados brutos não podem ser processados por bibliotecas modernas de FFT — o ambiente de validação é isolado e só permite operações aritméticas básicas. A equipe precisa reimplementar a Transformada de Fourier Discreta 2D a partir da própria definição matemática, célula por célula, para depois comparar bit a bit com np.fft.fft2 em outro ambiente.

Este é o exercício mais conceitual da lista: não há atalhos. Você vai implementar o duplo somatório da Equação 5.1 diretamente, evidenciando por que a FFT existe — e o custo computacional que ela evita.

5.13.4.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(M\) (linhas) e \(N\) (colunas) da imagem \(f(x,y)\).
  2. Dados: Ler os valores inteiros de \(f(x,y)\), linha a linha.
  3. DFT 2D: Para cada par de frequências \((u,v)\) com \(u=0,\ldots,M-1\) e \(v=0,\ldots,N-1\), calcular \[ 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)} \] usando a identidade de Euler \(e^{-j\theta} = \cos(\theta) - j\sin(\theta)\) para separar parte real e imaginária — não utilize nenhuma função de FFT pronta.
  4. Magnitude: Calcular \(|F(u,v)| = \sqrt{\text{Re}(F)^2 + \text{Im}(F)^2}\) e arredondar para o inteiro mais próximo.
  5. Saída: Exibir a matriz de magnitudes arredondadas, \(M \times N\), na mesma ordem (sem fftshift — o DC permanece em \((0,0)\)).

5.13.4.2 📌 Restrições Computacionais

  • Proibido usar bibliotecas de FFT: a implementação deve calcular os somatórios duplos explicitamente (laços aninhados), mesmo que mais lenta.
  • Sem fftshift: a saída mantém a convenção crua da DFT, com o componente DC em \(F(0,0)\) (canto superior esquerdo).
  • Arredondamento: a magnitude final deve ser arredondada para o inteiro mais próximo; nos casos de teste não há ambiguidade .5.
  • Precisão: pequenos erros de ponto flutuante (ordem de \(10^{-6}\)) antes do arredondamento são esperados e não afetam o resultado inteiro final.

5.13.4.3 🧠 Fundamentação Teórica

Elemento Significado
\(F(0,0)\) Componente DC — soma de todos os pixels, \(F(0,0) = \sum f(x,y)\)
Parte real \(\text{Re}(F)\) Projeção do sinal sobre cossenos
Parte imaginária \(\text{Im}(F)\) Projeção do sinal sobre senos
Complexidade desta implementação \(\mathcal{O}((MN)^2)\) — por isso a FFT, com \(\mathcal{O}(MN\log(MN))\), é indispensável em imagens reais

5.13.4.4 📦 Especificação de Entrada e Saída (VPL)

Entrada:

  • Linha 1: Inteiro \(M\).
  • Linha 2: Inteiro \(N\).
  • Linhas seguintes: Elementos inteiros de \(f(x,y)\), \(M\) linhas.

Saída:

  • Matriz de magnitudes \(|F(u,v)|\) arredondadas, \(M \times N\), separadas por espaço.

5.13.4.5 📌 Exemplos

Entrada Saída Observação
2
2
1 2
3 4
10 2
4 0
\(F(0,0)=1+2+3+4=10\) (DC = soma total). \(F(0,1)=(1-2)+(3-4)=-2 \to |F|=2\). \(F(1,0)=(1+2)-(3+4)=-4\to|F|=4\). \(F(1,1)=(1-2)-(3-4)=0\).
🎮 Simulador EP05_04: DFT 2D — Definição Direta ΣΣ f(x,y) e-j2π(…)
Clique nas células de f(x,y) para alterar os valores (incrementa +1; Shift + clique decrementa -1) e veja |F(u,v)| recalculado ao vivo.
f(x,y) — Domínio Espacial
|F(u,v)| — Magnitude (Sem Shift)
–
Figura 5.35: Simulador EP05_04: DFT 2D manual
%%writefile EP05_04.cpp
// sua solução
Overwriting EP05_04.cpp
TestSuite("EP05_04.cpp").run()
✔️ EP05_04.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP05_04.cases

🔍 Testando C++: EP05_04.cpp
⚠️ EP05_04.cpp: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

5.13.5 EP05_05 🏆 Pipeline JPEG Completo: DCT, Quantização e Reconstrução

Você foi contratado para criar, do zero, um codec JPEG didático em ambiente embarcado, sem qualquer biblioteca de imagem disponível — apenas operações matemáticas básicas. O cliente quer entender exatamente onde a qualidade é perdida e onde ela é recuperada, bloco por bloco. Este é o desafio final do capítulo: integrar tudo o que foi estudado — a DCT-II ortonormal, a quantização perceptual e a reconstrução via IDCT — em um único pipeline de ponta a ponta, processando um bloco \(N \times N\) do início ao fim, exatamente como o padrão JPEG faz internamente, \(8\times8\) pixels de cada vez.

5.13.5.1 📋 Diretrizes de Implementação

  1. Dimensão do bloco: Ler o inteiro \(N\).
  2. Bloco original: Ler a matriz de pixels \(f(x,y)\), \(N\) linhas com \(N\) inteiros em \([0,255]\).
  3. Tabela de quantização: Ler a matriz \(Q\), \(N \times N\) inteiros positivos.
  4. Centralização: Subtrair 128 de cada pixel: \(g(x,y) = f(x,y) - 128\).
  5. DCT-II 2D ortonormal: Calcular \[ C(u,v) = \alpha(u)\,\alpha(v)\sum_{x=0}^{N-1}\sum_{y=0}^{N-1} g(x,y)\,\cos\!\left[\frac{\pi(2x+1)u}{2N}\right]\cos\!\left[\frac{\pi(2y+1)v}{2N}\right] \] com \(\alpha(0)=\sqrt{1/N}\) e \(\alpha(k)=\sqrt{2/N}\) para \(k>0\).
  6. Quantização: \(\tilde{C}(u,v) = \text{round}(C(u,v)/Q(u,v))\).
  7. Desquantização: \(C'(u,v) = \tilde{C}(u,v)\times Q(u,v)\).
  8. IDCT-II 2D (inversa ortonormal): Calcular \(g'(x,y)\) a partir de \(C'(u,v)\) usando a transformada inversa correspondente (mesma base, somatório sobre \(u,v\)).
  9. Reversão da centralização e arredondamento: \(f'(x,y) = \text{round}(g'(x,y) + 128)\), restrito ao intervalo \([0,255]\) (clipping).
  10. Saída: Exibir o bloco reconstruído \(f'\), \(N \times N\), inteiros.

5.13.5.2 📌 Restrições Computacionais

  • Pipeline completo obrigatório: todas as seis etapas (centralizar, DCT, quantizar, desquantizar, IDCT, reverter) devem ser implementadas — pular a quantização não passa nos testes, pois o resultado seria idêntico ao original.
  • Clipping: valores reconstruídos fora de \([0,255]\) devem ser truncados (0 se negativo, 255 se maior que 255).
  • Arredondamento: tanto na quantização quanto na reconstrução final dos pixels, use arredondamento padrão; os casos de teste evitam ambiguidade .5.
  • Base ortonormal: a normalização \(\alpha(u)\) e \(\alpha(v)\) deve ser aplicada exatamente como especificado — sem ela, a IDCT não reconstrói corretamente.

5.13.5.3 🧠 Fundamentação Teórica

Etapa Análoga no padrão JPEG real Onde a qualidade é perdida
Centralização Mesma — DCT assume sinal centrado em zero Nenhuma perda
DCT-II Etapa 3–4 do pipeline (Tabela 5.7) Nenhuma perda (transformação exata e reversível)
Quantização Etapa 5 — divisão por \(Q(u,v)\) Principal fonte de perda — coeficientes de alta frequência viram zero
IDCT Reconstrução final Reconstrói exatamente os coeficientes quantizados, não os originais

5.13.5.4 📦 Especificação de Entrada e Saída (VPL)

Entrada:

  • Linha 1: Inteiro \(N\).
  • \(N\) linhas seguintes: bloco original \(f(x,y)\), inteiros em \([0,255]\).
  • \(N\) linhas seguintes: tabela de quantização \(Q\), inteiros positivos.

Saída:

  • Bloco reconstruído \(f'(x,y)\), \(N \times N\), inteiros em \([0,255]\), separados por espaço.

5.13.5.5 📌 Exemplos

Entrada Saída Observação
4
120 130 125 128
115 140 135 122
118 150 160 130
110 120 145 138
4 6 8 10
6 8 10 12
8 10 12 16
10 12 16 20
118 126 119 131
114 143 140 119
117 149 159 130
107 121 146 139
Após DCT, quantização agressiva nas altas frequências (valores grandes de \(Q\) no canto inferior direito) e reconstrução via IDCT, o bloco fica próximo do original, mas não idêntico — a diferença é o custo da compressão lossy.

5.13.5.6 💡 Dica de Depuração

Se o resultado não bater, verifique nesta ordem: (1) os coeficientes DCT brutos (antes da quantização) — eles devem reconstruir o original exatamente via IDCT se você pular a etapa 6–7; (2) a tabela \(\alpha(u)\) — erro comum é aplicar \(\sqrt{2/N}\) também para \(u=0\); (3) o arredondamento da quantização, que deve ocorrer antes de multiplicar de volta por \(Q\).

🎮 Simulador EP05_05: Pipeline JPEG (Bloco 4×4) DCT → Q → IDCT
Ajuste o fator de escala de quantização e observe o bloco reconstruído se afastar (ou se aproximar) do original.
Bloco Original
Reconstruído (DCT → Q → IDCT)
–
Figura 5.36: Simulador EP05_05: Pipeline JPEG completo em bloco
%%writefile EP05_05.cpp
// sua solução
Overwriting EP05_05.cpp
TestSuite("EP05_05.cpp").run()
✔️ EP05_05.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP05_05.cases

🔍 Testando C++: EP05_05.cpp
⚠️ EP05_05.cpp: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.