3  Operações Espaciais: Intensidade, Histograma e Filtragem

Este capítulo aprofunda o processamento de imagens no domínio espacial, partindo da manipulação direta de pixels e histogramas para o realce de contraste, até a aplicação de filtros locais por convolução para suavização, redução de ruído e detecção de bordas. O objetivo é desenvolver a intuição matemática e computacional que sustenta grande parte dos algoritmos modernos de Visão Computacional.

3.1 Objetivos

Ao final deste capítulo, você será capaz de:

  • Manipular intensidade e pixels: Executar operações aritméticas saturadas (mm::addm, mm::subm) e lógicas bit a bit (mm::band, mm::bor, mm::bnot) para combinação e seleção de regiões de interesse (ROI), e aplicar alpha blending (mm::blend) para fusão ponderada de imagens;
  • Processar histogramas: Interpretar o histograma como diagnóstico tonal e aplicar equalização global via CDF (mm::equalize); na trilha Python, também a equalização adaptativa (CLAHE) e a especificação de histograma para transferência de perfil tonal entre imagens;
  • Compreender fundamentos espaciais: Entender vizinhança, padding de borda (mm::pad) e a diferença entre correlação cruzada (mm::conv) e convolução — incluindo por que kernels assimétricos como o de Sobel produzem resultados distintos nas duas operações;
  • Aplicar filtragem de suavização: Usar o filtro de média (mm::blur, ou mm::conv com kernel uniforme) e o filtro Gaussiano (mm::gaussian) para redução de ruído, compreendendo a vantagem da ponderação radial e da separabilidade Gaussiana;
  • Aplicar filtragem de realce: Usar o Laplaciano \(w_4\) e \(w_8\) (mm::laplacian) para realce isotrópico de bordas, o operador de Sobel (mm::sobel) para a magnitude do gradiente — e, na trilha Python, a decomposição direcional \(G_x\), \(G_y\) e ângulo —, e o Unsharp Masking (mm::usm) para amplificação de alta frequência controlada pelo parâmetro \(k\);
  • Utilizar filtros de ordem: Aplicar o filtro da mediana (mm::median) para remoção de ruído sal e pimenta, compreendendo por que sua natureza não linear e a robustez a outliers o tornam superior aos filtros lineares nesse cenário;
  • Resolver problemas práticos: Encadear técnicas em pipelines de pré-processamento (equalização → Gaussiano → Canny; com CLAHE no lugar da equalização na trilha Python) e usar as funções da morph (mm::conv, mm::histImg, mm::equalize, mm::drawImgKernel) para análise e visualização didática de cada etapa.

3.2 Operações em Nível de Intensidade

O nível mais elementar de processamento de imagens atua diretamente sobre os valores dos pixels, sem considerar vizinhança. Essas operações — chamadas de transformações de ponto (point operations) — são as mais rápidas computacionalmente e formam a base para técnicas mais complexas.

Formalmente, uma transformação de ponto pode ser descrita como:

\[ g(x,y) = T[f(x,y)] \tag{3.1}\]

onde \(f(x,y)\) é a imagem de entrada, \(g(x,y)\) é a saída e \(T\) é uma função aplicada a cada pixel individualmente.

3.2.1 Preparando o Ambiente Prático

O bloco a seguir carrega a biblioteca morph do repositório (o módulo morph.py e, na trilha C++, também a morph.hpp usada no #include das células compiladas).

import os, urllib.request

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

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")

# O kernel é Python mesmo na trilha C++: `mm` (morph.py) é usado pelos
# simuladores, pela exibição das figuras que o binário C++ gera e pelo
# estado mm::Image entre células. cpp=True baixa também a trilha compilada
# (morph.hpp + stb_image*.h), usada no #include das células %%writefile *.cpp.
import config
config.setup(cpp=True)
from morph import mm
import numpy as np
✅ Ambiente pronto. Morph: 1.1.9 | OpenCV: 5.0.0

Como objeto de estudo ao longo deste capítulo, utilizaremos as imagens de vida selvagem apresentadas nas Figura 3.1 e Figura 3.3. A partir delas, exploraremos operações espaciais sobre intensidade, histogramas e filtragem, analisando seus efeitos no realce, na suavização, na redução de ruído e na detecção de bordas, de modo a compreender os fundamentos matemáticos e computacionais do PDI.

%%writefile tmp/fig_03_mandrill.cpp
#define MM_OUT "tmp/fig_03_mandrill.png"
// Compile: g++ -std=c++17 -o programa programa.cpp -I. -lstdc++fs

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

int main() {
    // | label: fig-03-mandrill
    // | fig-cap: "*Mandrill* (*Mandrillus sphinx*) fotografado em ambiente natural na África do Sul. Crédito: Carlos Guilherme Rodrigues (CC BY-SA 3.0)."
    // | echo: true

    std::string base    = "https://upload.wikimedia.org/wikipedia/commons";
    std::string arquivo = "Carlos_Guilherme_Rodrigues_%2876515283%29.jpeg";
    std::string url     = base + "/9/9b/" + arquivo;
    std::string caminho = "imagens/mandrill-exif.jpg";

    if (!std::filesystem::exists(caminho)) {
        std::filesystem::create_directories("imagens");
        mm::write(mm::read(url), caminho);
    }

    mm::Image img_color = mm::read(caminho);
    mm::Image img_gray  = mm::gray(img_color);

    mm::show(img_color, MM_OUT);

    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img_gray, "tmp/state/img_gray_8.png");
// [pdi:state-io:end]
return 0;
}
Overwriting tmp/fig_03_mandrill.cpp
!g++ -I. -std=c++17 tmp/fig_03_mandrill.cpp -o tmp/fig_03_mandrill \
  && ./tmp/fig_03_mandrill \
  && test -f "tmp/fig_03_mandrill.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_mandrill.png"
try:
    mm.show(mm.read("tmp/fig_03_mandrill.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_mandrill.png (ver a versao Python)")
Figura 3.1: Mandrill (Mandrillus sphinx) fotografado em ambiente natural na África do Sul. Crédito: Carlos Guilherme Rodrigues (CC BY-SA 3.0).

3.2.2 Operações Aritméticas

Operações aritméticas entre imagens são amplamente usadas em PDI para combinar, comparar ou realçar informações. A subtração de imagens é especialmente poderosa para detectar diferenças entre dois quadros — por exemplo, na remoção de fundo estático em câmeras de vigilância:

\[ g(x,y) = f_1(x,y) - f_2(x,y) \tag{3.2}\]

A adição saturada limita o resultado ao intervalo \([0, 255]\): valores acima de 255 são fixados em 255, evitando o overflow silencioso do tipo uint8 (ex.: \(200 + 100 = 44\) em vez de 300). A subtração saturada aplica o mesmo princípio pelo lado inferior: valores negativos são fixados em 0.

AvisoSaturação e overflow

Operações aritméticas em uint8 sofrem overflow silencioso: \(200 + 100 = 44\) (não 300). mm::addm e mm::subm fazem a saturação automática, fixando o resultado em \([0, 255]\). O blending usa pesos fracionários: mm::blend opera internamente em ponto flutuante e só então arredonda e satura para uint8.

A Figura 3.2 demonstra adição de uma constante (clareamento) e subtração de uma constante (escurecimento com saturação em 0).

%%writefile tmp/fig_03_aritmetica.cpp
#define MM_OUT "tmp/fig_03_aritmetica.png"
#include <iostream>
#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_8.png");
// [pdi:state-io:end]

    // Variáveis fornecidas automaticamente:
    // img_gray (mm::Image)

    int fundo = 60;

    mm::Image img_add = mm::addm(img_gray, fundo);
    mm::Image img_sub = mm::subm(img_gray, fundo);

    mm::show(
        std::vector<mm::Image>{img_gray, img_add, img_sub},
        MM_OUT,
        std::vector<std::string>{"Original", "addm (+60)", "subm (−60)"},
        3
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_03_aritmetica_0.png");
mm::write(img_add, "tmp/fig_03_aritmetica_1.png");
mm::write(img_sub, "tmp/fig_03_aritmetica_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_aritmetica.cpp
!g++ -I. -std=c++17 tmp/fig_03_aritmetica.cpp -o tmp/fig_03_aritmetica \
  && ./tmp/fig_03_aritmetica \
  && test -f "tmp/fig_03_aritmetica.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_aritmetica.png"
[1] Original
[2] addm (+60)
[3] subm (−60)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_aritmetica_0.png"),
            mm.read("tmp/fig_03_aritmetica_1.png"),
            mm.read("tmp/fig_03_aritmetica_2.png"),
        ],
        titles=[
            'Original',
            'addm (+60)',
            'subm (−60)',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_aritmetica_0.png (ver a versao Python)")
Figura 3.2: Operações aritméticas saturadas: adição de constante (clareamento) e subtração de constante (escurecimento com saturação em 0).

3.2.3 Mistura Ponderada (Alpha Blending)

A mistura ponderada (alpha blending) combina duas imagens utilizando pesos complementares \(\alpha\) e \((1-\alpha)\):

\[ g(x,y) = \alpha\,f_1(x,y) + (1-\alpha)\,f_2(x,y), \quad \alpha \in [0,1] \tag{3.3}\]

Quando \(\alpha = 1\), obtém-se apenas a imagem \(f_1\); quando \(\alpha = 0\), apenas \(f_2\). Valores intermediários produzem uma transição suave entre ambas, sendo amplamente utilizados em composição de imagens, sobreposição de camadas, marcas d’água e efeitos de fusão visual.

Para que a combinação produza um resultado coerente, é necessário alinhar previamente as regiões de interesse. Na Figura 3.4, recorta-se o rosto do leopardo com mm::crop(img_leop_gray, 250, H-300, 100, W-200) e a região facial do mandril com mm::crop(img_gray, 100, 400, 380, 530), de modo que olhos e estrutura facial fiquem aproximadamente alinhados. O recorte do leopardo é então redimensionado (mm::resize) para as dimensões do mandril antes da mistura.

mm::blend faz a operação em ponto flutuante — evitando overflow nas contas com pesos fracionários — e só então arredonda e satura o resultado para uint8.

%%writefile tmp/fig_03_leopardo.cpp
#define MM_OUT "tmp/fig_03_leopardo.png"
#include "morph.hpp"
#include <iostream>
#include <filesystem>

int main() {
    //| label: fig-03-leopardo
    //| fig-cap: "Retrato de um leopardo (*Panthera pardus*) em ambiente natural. Crédito: C. Brück (CC BY-SA 4.0)."
    //| echo: true

    const std::string base    = "https://upload.wikimedia.org/wikipedia/commons";
    const std::string arquivo = "Leopard_%28Panthera_pardus%29_portrait.jpg";
    const std::string url     = base + "/9/92/" + arquivo;

    mm::Image img_leop      = mm::read(url);
    mm::Image img_leop_gray = mm::gray(img_leop);

    mm::show(img_leop, MM_OUT);

    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img_leop, "tmp/state/img_leop_12.png");
mm::write(img_leop_gray, "tmp/state/img_leop_gray_12.png");
// [pdi:state-io:end]
return 0;
}
Overwriting tmp/fig_03_leopardo.cpp
!g++ -I. -std=c++17 tmp/fig_03_leopardo.cpp -o tmp/fig_03_leopardo \
  && ./tmp/fig_03_leopardo \
  && test -f "tmp/fig_03_leopardo.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_leopardo.png"
try:
    mm.show(mm.read("tmp/fig_03_leopardo.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_leopardo.png (ver a versao Python)")
Figura 3.3: Retrato de um leopardo (Panthera pardus) em ambiente natural. Crédito: C. Brück (CC BY-SA 4.0).
%%writefile tmp/fig_03_blend.cpp
#define MM_OUT "tmp/fig_03_blend.png"
//| label: fig-03-blend
//| fig-cap: "*Alpha blending* entre recortes alinhados de mandrill e do leopardo (@fig-03-leopardo) para diferentes valores de α. Em α=1 vê-se apenas mandrill; em α=0, apenas o leopardo; valores intermediários fundem os olhares das duas imagens proporcionalmente."
//| echo: true
//| output: true

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

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

    // recortes alinhados: rosto do leopardo e região facial do mandril
    mm::Image leo      = mm::crop(img_leop_gray, 250, img_leop_gray.h - 300, 100, img_leop_gray.w - 200);
    mm::Image mandrill = mm::crop(img_gray, 100, 400, 380, 530);
    mm::Image leo_r    = mm::resize(leo, mandrill.w, mandrill.h, "bilinear");

    mm::show(
        {mm::blend(mandrill, leo_r, 1.0), mm::blend(mandrill, leo_r, 0.8),
         mm::blend(mandrill, leo_r, 0.6), mm::blend(mandrill, leo_r, 0.4),
         mm::blend(mandrill, leo_r, 0.2), mm::blend(mandrill, leo_r, 0.0)},
        MM_OUT,
        {"α=1.0", "α=0.8", "α=0.6", "α=0.4", "α=0.2", "α=0.0"},
        6
    );

    return 0;
}
Overwriting tmp/fig_03_blend.cpp
!g++ -I. -std=c++17 tmp/fig_03_blend.cpp -o tmp/fig_03_blend \
  && ./tmp/fig_03_blend \
  && test -f "tmp/fig_03_blend.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_blend.png"
[1] α=1.0
[2] α=0.8
[3] α=0.6
[4] α=0.4
[5] α=0.2
[6] α=0.0
try:
    mm.show(mm.read("tmp/fig_03_blend.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_blend.png (ver a versao Python)")
Figura 3.4: Alpha blending entre recortes alinhados de mandrill e do leopardo (Figura 3.3) para diferentes valores de α. Em α=1 vê-se apenas mandrill; em α=0, apenas o leopardo; valores intermediários fundem os olhares das duas imagens proporcionalmente.

3.2.4 Operações Lógicas e Máscaras Bit a Bit

As operações lógicas bit a bit (AND, OR e NOT) atuam diretamente sobre os bits de cada pixel e são a base para criação e aplicação de máscaras (masks) — imagens binárias com apenas 0 (preto) e 255 (branco) usadas para isolar Regiões de Interesse (ROI).

O comportamento de cada operação decorre da representação binária do 255 (11111111) e do 0 (00000000):

  • AND com a máscara: onde \(m = 255\), os bits originais são preservados; onde \(m = 0\), o pixel é zerado. Resultado: recorte da ROI. \[g(x,y) = f(x,y) \;\text{AND}\; m(x,y) \tag{3.4}\]
  • OR com a máscara: onde \(m = 255\), o pixel é forçado a branco; onde \(m = 0\), o valor original é mantido. Resultado: iluminação da ROI.
  • NOT (sem máscara): inverte todos os bits (\(g = 255 - f\)), produzindo o negativo fotográfico da imagem.

A Figura 3.5 ilustra as três operações aplicadas à imagem do mandrill com uma máscara circular.

%%writefile tmp/fig_03_logica.cpp
#define MM_OUT "tmp/fig_03_logica.png"
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#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_8.png");
// [pdi:state-io:end]

    // img_gray já está inicializado

    int h = img_gray.h;
    int w = img_gray.w;

    // Máscara circular preenchida, centrada na imagem (mm.circle desenha o
    // disco; a versão didática, teste de raio pixel a pixel, é mm.circle0).
    mm::Image mask_circ(h, w);
    mask_circ = mm::circle(mask_circ, w / 2, h / 2, std::min(h, w) / 3 - 10, 255, -1);

    // Operações via morph
    mm::Image img_not = mm::bnot(img_gray);            // NOT: negativo fotográfico
    mm::Image img_and = mm::band(img_gray, mask_circ); // preserva apenas a ROI circular
    mm::Image img_or  = mm::bor(img_gray, mask_circ);  // ilumina a região da máscara

    mm::show(
        std::vector<mm::Image>{img_gray, img_and, img_or, img_not},
        MM_OUT,
        std::vector<std::string>{"Original", "AND (ROI circular)", "OR (ilumina ROI)", "NOT (negativo)"},
        4
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_03_logica_0.png");
mm::write(img_and, "tmp/fig_03_logica_1.png");
mm::write(img_or, "tmp/fig_03_logica_2.png");
mm::write(img_not, "tmp/fig_03_logica_3.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_logica.cpp
!g++ -I. -std=c++17 tmp/fig_03_logica.cpp -o tmp/fig_03_logica \
  && ./tmp/fig_03_logica \
  && test -f "tmp/fig_03_logica.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_logica.png"
[1] Original
[2] AND (ROI circular)
[3] OR (ilumina ROI)
[4] NOT (negativo)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_logica_0.png"),
            mm.read("tmp/fig_03_logica_1.png"),
            mm.read("tmp/fig_03_logica_2.png"),
            mm.read("tmp/fig_03_logica_3.png"),
        ],
        titles=[
            'Original',
            'AND (ROI circular)',
            'OR (ilumina ROI)',
            'NOT (negativo)',
        ],
        cols=4,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_logica_0.png (ver a versao Python)")
Figura 3.5: Operações lógicas bit a bit com máscara circular: AND (isolamento da ROI), OR (iluminação da ROI) e NOT (negativo).

3.3 Histograma de Imagens

O histograma de uma imagem em tons de cinza é uma função discreta que descreve a distribuição de frequências das intensidades:

\[ h(r_k) = n_k, \quad k = 0, 1, \ldots, L-1 \tag{3.5}\]

onde \(r_k\) é o \(k\)-ésimo nível de intensidade, \(n_k\) é o número de pixels com essa intensidade e \(L\) é o total de níveis (tipicamente 256 para 8 bits). O histograma normalizado estima a probabilidade de cada nível:

\[ p(r_k) = \frac{n_k}{MN} \tag{3.6}\]

onde \(MN\) é o total de pixels. Por ser uma estatística global, o histograma não carrega informação posicional, mas revela características essenciais como brilho médio, contraste e distribuição tonal. Na prática, mm::hist(img) retorna o vetor de contagens \(h(r_k)\), que serve tanto para visualização (via mm::histImg) quanto para cálculos como função de distribuição acumulada (CDF) e equalização.

NotaInterpretação do Histograma
  • Estreito à esquerda: imagem subexposta (escura).
  • Estreito à direita: imagem superexposta (clara).
  • Concentrado no centro: baixo contraste.
  • Distribuído por toda a faixa: alto contraste, boa utilização dos tons disponíveis.

A Figura 3.6 apresenta o histograma da imagem do mandrill, bem como versões escurecida (mm::subm) e clareada (mm::addm). Observa-se o deslocamento da distribuição de intensidades para a esquerda e para a direita, respectivamente. Note que o intervalo representado no eixo \(x\) não corresponde necessariamente a toda a faixa de 0 a 255.

%%writefile tmp/fig_03_histograma.cpp
#define MM_OUT "tmp/fig_03_histograma.png"
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>

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

    // img_gray já está inicializado (inserido automaticamente)

    // Versão escurecida (-80) e clareada (+80) com saturação
    mm::Image img_dark = mm::subm(img_gray, 80);
    mm::Image img_high = mm::addm(img_gray, 80);

    // Exibição das imagens e seus histogramas
    mm::show(
        std::vector<mm::Image>{img_gray, img_dark, img_high,
                               mm::histImg(img_gray), mm::histImg(img_dark), mm::histImg(img_high)},
        MM_OUT,
        std::vector<std::string>{"Original", "Escurecida (-80)", "Clareada (+80)",
                                 "Histograma - original", "Histograma - escurecida", "Histograma - clareada"},
        3
    );

    return 0;
}
Overwriting tmp/fig_03_histograma.cpp
!g++ -I. -std=c++17 tmp/fig_03_histograma.cpp -o tmp/fig_03_histograma \
  && ./tmp/fig_03_histograma \
  && test -f "tmp/fig_03_histograma.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_histograma.png"
[1] Original
[2] Escurecida (-80)
[3] Clareada (+80)
[4] Histograma - original
[5] Histograma - escurecida
[6] Histograma - clareada
try:
    mm.show(mm.read("tmp/fig_03_histograma.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_histograma.png (ver a versao Python)")
Figura 3.6: Histogramas da imagem original, de uma versão escurecida (−80) e de uma clareada (+80). A subtração/adição satura em 0 e 255.

3.3.1 Equalização de Histograma

A equalização de histograma redistribui as intensidades para que o histograma resultante seja o mais uniforme possível. O mapeamento é dado pela função de distribuição acumulada (CDF):

\[ s_k = T(r_k) = (L-1)\sum_{j=0}^{k} p(r_j) = \frac{L-1}{MN}\sum_{j=0}^{k} n_j \tag{3.7}\]

A transformação é monotônica: níveis frequentes recebem intervalos maiores no domínio de saída (maior separação → mais contraste), enquanto níveis raros são comprimidos.

O algoritmo completo, em cinco etapas, é apresentado na Tabela 3.1.

Tabela 3.1: Algoritmo de equalização de histograma.
Etapa Operação Fórmula
1 Histograma \(h[k] \leftarrow\) número de pixels com intensidade \(k\), \(k=0\ldots L-1\)
2 Probabilidade \(p[k] \leftarrow h[k] / MN\)
3 CDF \(\text{cdf}[k] \leftarrow \sum_{j=0}^{k} p[j]\) (soma acumulada)
4 Look-Up Table (mapeamento) \(\text{lut}[k] \leftarrow \text{round}(\text{cdf}[k] \times (L-1))\)
5 Aplicação \(g[i,j] \leftarrow \text{lut}[f[i,j]]\) (para todo pixel)

Note na Figura 3.7 que a equalização redistribui os tons existentes para posições mais espaçadas na faixa \([0, L-1]\), mas não cria novos tons — a imagem equalizada continua com exatamente 3 tons distintos, agora em \(\{1, 5, 7\}\) em vez de \(\{2, 3, 4\}\).

%%writefile tmp/fig_03_equalizacao_didatica.cpp
#define MM_OUT "tmp/fig_03_equalizacao_didatica.png"
//| label: fig-03-equalizacao-didatica
//| fig-cap: "Equalização de histograma numa imagem 5×5 de 3 bits (L=8): tons concentrados em {2,3,4} são redistribuídos pela CDF. *mm::equalize(img, 3)* faz o mapeamento."
//| echo: true
//| output: true

#include "morph.hpp"
#include <iostream>

int main() {
    mm::Image img5(5, 5);
    // preenche a imagem 5x5 com os valores especificados
    unsigned char vals[5][5] = {
        {3, 4, 2, 3, 4},
        {4, 3, 3, 4, 3},
        {2, 3, 4, 3, 2},
        {3, 4, 3, 2, 3},
        {4, 3, 2, 3, 4}
    };
    for (int y = 0; y < 5; ++y)
        for (int x = 0; x < 5; ++x)
            img5.at(y, x) = vals[y][x];

    mm::Image img5_eq = mm::equalize(img5, 3);   // L = 2^3 = 8

    std::cout << "Imagem original 5x5 (3 bits):\n";
    std::cout << mm::drawImg(img5) << "\n";
    std::cout << "Imagem equalizada 5x5:\n";
    std::cout << mm::drawImg(img5_eq) << "\n";

    mm::show(std::vector<mm::Image>{img5, img5_eq, mm::histImg(img5), mm::histImg(img5_eq)},
             MM_OUT,
             std::vector<std::string>{"Original", "Equalizada", "Histograma - original", "Histograma - equalizada"},
             2);

    return 0;
}
Overwriting tmp/fig_03_equalizacao_didatica.cpp
!g++ -I. -std=c++17 tmp/fig_03_equalizacao_didatica.cpp -o tmp/fig_03_equalizacao_didatica \
  && ./tmp/fig_03_equalizacao_didatica \
  && test -f "tmp/fig_03_equalizacao_didatica.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_equalizacao_didatica.png"
Imagem original 5x5 (3 bits):
3 4 2 3 4 
4 3 3 4 3 
2 3 4 3 2 
3 4 3 2 3 
4 3 2 3 4 

Imagem equalizada 5x5:
5 7 1 5 7 
7 5 5 7 5 
1 5 7 5 1 
5 7 5 1 5 
7 5 1 5 7 

[1] Original
[2] Equalizada
[3] Histograma - original
[4] Histograma - equalizada
try:
    mm.show(mm.read("tmp/fig_03_equalizacao_didatica.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_equalizacao_didatica.png (ver a versao Python)")
Figura 3.7: Equalização de histograma numa imagem 5×5 de 3 bits (L=8): tons concentrados em {2,3,4} são redistribuídos pela CDF. mm::equalize(img, 3) faz o mapeamento.

Limitação: a equalização global pode super-realçar ruídos e produzir contraste excessivo em regiões homogêneas. O CLAHE (Contrast Limited Adaptive Histogram Equalization) reduz esse problema ao aplicar a equalização em blocos locais (tiles) e limitar a altura dos picos do histograma antes da equalização.

A Figura 3.8 compara a imagem original, a equalização global via mm::equalize e o CLAHE do OpenCV, exibindo também os histogramas resultantes. Diferentemente da equalização global, que utiliza uma única transformação baseada na CDF de toda a imagem, o CLAHE adapta o contraste a cada região, sendo particularmente útil em imagens com iluminação não uniforme.

No exemplo, foi utilizado clipLimit=2.0 e tileGridSize=(32,32). O parâmetro clipLimit define o quanto os picos do histograma local podem crescer antes de serem cortados (clipped). No OpenCV, esse valor é um fator relativo: o limite real é aproximadamente calculado como clipLimit × (número de pixels do bloco / número de níveis de cinza). Por exemplo, em um bloco com 4096 pixels e uma imagem de 8 bits (256 níveis de cinza), a frequência média por nível é \(4096/256=16\). Assim, clipLimit=2.0 permite picos de aproximadamente \(2\times16=32\) ocorrências antes do corte. As ocorrências excedentes não são descartadas: elas são redistribuídas entre os demais níveis de cinza do histograma, reduzindo a concentração excessiva em poucos níveis e evitando uma amplificação exagerada do contraste local. Valores menores limitam mais o contraste e reduzem a amplificação de ruído, enquanto valores maiores permitem um realce mais intenso, mas podem introduzir artefatos.

  • clipLimit=1.0: realce suave e conservador;
  • clipLimit=2.0: bom equilíbrio entre contraste e naturalidade;
  • clipLimit=4.0: maior destaque para detalhes locais;
  • clipLimit=8.0: contraste agressivo, com possível amplificação de ruído.

Assim, o CLAHE costuma produzir resultados mais naturais do que a equalização global, especialmente em imagens com sombras, reflexos ou iluminação desigual.

%%writefile tmp/fig_03_equalizacao.cpp
#define MM_OUT "tmp/fig_03_equalizacao.png"
//| label: fig-03-equalizacao
//| fig-cap: "Equalização de histograma global (mm::equalize, via CDF) e os histogramas antes/depois. CLAHE (adaptativa) fica só na trilha Python — não tem equivalente em morph.hpp."
//| echo: true
//| output: true

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

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

    mm::Image img_eq = mm::equalize(img_gray);

    mm::show(
        std::vector<mm::Image>{img_gray, img_eq, mm::histImg(img_gray), mm::histImg(img_eq)},
        MM_OUT,
        std::vector<std::string>{"Original", "mm.equalize (CDF)", "Histograma - original", "Histograma - equalizado"},
        2
    );

    return 0;
}
Overwriting tmp/fig_03_equalizacao.cpp
!g++ -I. -std=c++17 tmp/fig_03_equalizacao.cpp -o tmp/fig_03_equalizacao \
  && ./tmp/fig_03_equalizacao \
  && test -f "tmp/fig_03_equalizacao.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_equalizacao.png"
[1] Original
[2] mm.equalize (CDF)
[3] Histograma - original
[4] Histograma - equalizado
try:
    mm.show(mm.read("tmp/fig_03_equalizacao.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_equalizacao.png (ver a versao Python)")
Figura 3.8: Equalização de histograma global (mm::equalize, via CDF) e os histogramas antes/depois. CLAHE (adaptativa) fica só na trilha Python — não tem equivalente em morph.hpp.

3.3.2 Especificação de Histograma

Enquanto a equalização impõe uma distribuição uniforme, a especificação de histograma (histogram matching) permite que o histograma da imagem de saída siga uma distribuição arbitrária — por exemplo, o histograma de outra imagem de referência.

O procedimento envolve três etapas:

  1. Calcular a CDF da imagem de entrada: \(P_r(r_k)\).
  2. Calcular a CDF da imagem de referência: \(P_z(z_k)\).
  3. Para cada nível \(r_k\), encontrar o nível \(z\) que minimiza \(|P_z(z) - P_r(r_k)|\).

\[ T(r_k) = \arg\min_{z}\,|P_z(z) - P_r(r_k)| \tag{3.8}\]

Na Figura 3.9, transferimos o perfil tonal do leopardo (Figura 3.3) para a imagem do mandrill — uma aplicação direta do conceito visto no blending: em vez de fundir pixels, aqui fundimos distribuições tonais.

%%writefile tmp/fig_03_especificacao.cpp
#define MM_OUT "tmp/fig_03_especificacao.png"
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>
#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_8.png");
mm::Image img_leop_gray = mm::_read_state("tmp/state/img_leop_gray_12.png");
// [pdi:state-io:end]

    // img_gray e img_leop_gray são fornecidos automaticamente

    // Equalização global do mandril
    mm::Image img_eq = mm::equalize(img_gray);

    // Exibição das imagens: original, referência e equalizada
    mm::show(std::vector<mm::Image>{img_gray, img_leop_gray, img_eq},
             MM_OUT,
             std::vector<std::string>{"Mandril (original)", "Leopardo (referencia)", "Mandril equalizado"},
             3);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_03_especificacao_0.png");
mm::write(img_leop_gray, "tmp/fig_03_especificacao_1.png");
mm::write(img_eq, "tmp/fig_03_especificacao_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_especificacao.cpp
!g++ -I. -std=c++17 tmp/fig_03_especificacao.cpp -o tmp/fig_03_especificacao \
  && ./tmp/fig_03_especificacao \
  && test -f "tmp/fig_03_especificacao.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_especificacao.png"
[1] Mandril (original)
[2] Leopardo (referencia)
[3] Mandril equalizado
try:
    mm.show(
        [
            mm.read("tmp/fig_03_especificacao_0.png"),
            mm.read("tmp/fig_03_especificacao_1.png"),
            mm.read("tmp/fig_03_especificacao_2.png"),
        ],
        titles=[
            'Mandril (original)',
            'Leopardo (referencia)',
            'Mandril equalizado',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_especificacao_0.png (ver a versao Python)")
Figura 3.9: Especificação de histograma (mapear o mandril para o perfil tonal do leopardo) precisa da CDF inversa da referência — fica só na trilha Python. Aqui, a equalização global do mandril, como comparação.

3.4 Fundamentos Espaciais: Vizinhança, Convolução e Kernels

As operações de filtragem espacial não atuam em um único pixel isolado, mas em uma vizinhança ao seu redor. Para isso, utiliza-se uma pequena matriz de coeficientes denominada kernel (ou máscara), que percorre toda a imagem por meio de uma janela deslizante (sliding window).

As janelas mais comuns são 3×3, 5×5 e 7×7. Em uma janela 3×3, por exemplo, o pixel central é processado juntamente com seus oito vizinhos imediatos. Em cada posição da janela, os valores dos pixels são combinados com os coeficientes do kernel, produzindo um novo valor para o pixel central.

3.4.1 Vizinhança

Considere uma janela 3×3 centrada no pixel \((x,y)\):

\[ \begin{bmatrix} (x-1,y-1) & (x,y-1) & (x+1,y-1) \\ (x-1,y) & (x,y) & (x+1,y) \\ (x-1,y+1) & (x,y+1) & (x+1,y+1) \end{bmatrix} \tag{3.9}\]

De forma geral, uma janela de tamanho \((2a+1)\times(2b+1)\) abrange todos os pixels situados até \(a\) posições na horizontal e até \(b\) posições na vertical em relação ao pixel central. Assim, uma janela 3×3 corresponde a \(a=b=1\), uma janela 5×5 a \(a=b=2\), e assim por diante.

Matematicamente, a vizinhança é definida por

\[ \mathcal{V}(x,y)= \{(x+s,\,y+t): -a\le s\le a,\,-b\le t\le b\} \tag{3.10}\]

3.4.2 Tratamento de Bordas

Pixels próximos às bordas possuem parte de sua vizinhança fora da imagem. Para aplicar filtros nessas regiões, é preciso definir como os valores externos serão obtidos. As três estratégias mais comuns (com a constante equivalente do OpenCV entre parênteses) são:

  • Zero-padding (BORDER_CONSTANT): completa a região externa com zeros.
  • Replicação (BORDER_REPLICATE): repete o valor do pixel da borda.
  • Reflexão (BORDER_REFLECT_101): espelha os pixels vizinhos, sem repetir o da borda.

mm::conv usa a reflexão por padrão, pois preserva melhor a continuidade dos níveis de cinza e reduz artefatos no tratamento das bordas.

O exemplo a seguir compara as três estratégias com mm::pad numa matriz 3×3. Observe como cada uma preenche os pixels externos necessários para aplicar um filtro 3×3 também nos cantos.

%%writefile tmp/mm_out_1.cpp
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>

int main() {
    // Criando a imagem 3x3 diretamente
    mm::Image img(3, 3);
    img.at(0,0) = 1; img.at(0,1) = 2; img.at(0,2) = 3;
    img.at(1,0) = 4; img.at(1,1) = 5; img.at(1,2) = 6;
    img.at(2,0) = 7; img.at(2,1) = 8; img.at(2,2) = 9;

    std::vector<std::string> bordas = {"constant", "replicate", "reflect101"};

    std::cout << "Imagem original:" << std::endl;
    std::cout << mm::drawImg(img) << std::endl;

    for (int k : {3, 5}) {
        int b = k / 2;
        std::cout << "=== Kernel " << k << "x" << k << " (padding b=" << b << ") ===" << std::endl;
        for (const std::string& nome : bordas) {
            std::cout << nome << std::endl;

            mm::Border border;
            if (nome == "constant") {
                border = mm::Border::CONSTANT;
            } else if (nome == "replicate") {
                border = mm::Border::REPLICATE;
            } else {
                border = mm::Border::REFLECT101;
            }

            std::cout << mm::drawImg(mm::pad(img, b, border)) << std::endl;
        }
    }

    return 0;
}
Overwriting tmp/mm_out_1.cpp
!g++ -I. -std=c++17 tmp/mm_out_1.cpp -o tmp/mm_out_1 \
  && ./tmp/mm_out_1
Imagem original:
1 2 3 
4 5 6 
7 8 9 

=== Kernel 3x3 (padding b=1) ===
constant
0 0 0 0 0 
0 1 2 3 0 
0 4 5 6 0 
0 7 8 9 0 
0 0 0 0 0 

replicate
1 1 2 3 3 
1 1 2 3 3 
4 4 5 6 6 
7 7 8 9 9 
7 7 8 9 9 

reflect101
5 4 5 6 5 
2 1 2 3 2 
5 4 5 6 5 
8 7 8 9 8 
5 4 5 6 5 

=== Kernel 5x5 (padding b=2) ===
constant
0 0 0 0 0 0 0 
0 0 0 0 0 0 0 
0 0 1 2 3 0 0 
0 0 4 5 6 0 0 
0 0 7 8 9 0 0 
0 0 0 0 0 0 0 
0 0 0 0 0 0 0 

replicate
1 1 1 2 3 3 3 
1 1 1 2 3 3 3 
1 1 1 2 3 3 3 
4 4 4 5 6 6 6 
7 7 7 8 9 9 9 
7 7 7 8 9 9 9 
7 7 7 8 9 9 9 

reflect101
9 8 7 8 9 8 7 
6 5 4 5 6 5 4 
3 2 1 2 3 2 1 
6 5 4 5 6 5 4 
9 8 7 8 9 8 7 
6 5 4 5 6 5 4 
3 2 1 2 3 2 1 

Observe que o resultado de um filtro pode variar significativamente conforme o tratamento adotado para as bordas da imagem.

Na morph.hpp, as funções de filtragem (mm::conv, mm::blur, mm::gaussian, mm::laplacian, mm::usm) aplicam padding por reflexão (mm::Border::REFLECT101) por padrão — o mesmo comportamento do cv2.filter2D. A variante didática mm::conv0 usa mm::Border::KEEP: os pixels da borda mantêm o valor original, sem o filtro. Já mm::sobel e mm::prewitt deixam a borda em zero (calculam apenas o interior).

3.4.3 Correlação vs. Convolução

Existem dois mecanismos matematicamente relacionados.

Correlação cruzada (cross-correlation) — o kernel é aplicado diretamente:

\[ g(x,y) = \sum_{s=-a}^{a}\sum_{t=-b}^{b} w(s,t)\,f(x+s,\,y+t) \tag{3.11}\]

Convolução bidimensional — o kernel é rotacionado 180° antes da aplicação:

\[ g(x,y) = \sum_{s=-a}^{a}\sum_{t=-b}^{b} w(s,t)\,f(x-s,\,y-t) \tag{3.12}\]

Para kernels simétricos (Gaussiano, Laplaciano, média) as duas operações produzem resultados idênticos. Para kernels assimétricos (Sobel, Prewitt) a diferença é significativa, como mostram os exemplos a seguir.

3.4.3.1 Correlação (mm::conv)

%%writefile tmp/mm_out_2.cpp
// Compile: g++ -std=c++17 -o programa programa.cpp -I. -lmorph

#include "morph.hpp"
#include <iostream>
#include <vector>

int main() {
    // imagem 4x4 (uint8) e kernel assimétrico 3x3
    mm::Image img(4, 4, 1);
    unsigned char dados[4][4] = {{ 1,  2,  3,  4},
                                 { 5,  6,  7,  8},
                                 { 9, 10, 11, 12},
                                 {13, 14, 15, 16}};
    for (int y = 0; y < 4; ++y)
        for (int x = 0; x < 4; ++x)
            img.at(y, x) = dados[y][x];

    mm::Kernel w{{0,1,2},{0,0,0},{0,0,0}};

    mm::Image corr = mm::conv(img, w, mm::Border::CONSTANT);  // zero fora da imagem

    std::cout << "Imagem original:" << std::endl;
    std::cout << mm::drawImg(img) << std::endl;
    std::cout << "Kernel:" << std::endl;
    // Como a função drawImg espera mm::Image, precisamos converter o kernel
    mm::Image kernel_img(3, 3, 1);
    for (int y = 0; y < 3; ++y)
        for (int x = 0; x < 3; ++x)
            kernel_img.at(y, x, 0) = (unsigned char)w.at(y, x);
    std::cout << mm::drawImg(kernel_img) << std::endl;
    std::cout << "Resultado da correlação:" << std::endl;
    std::cout << mm::drawImg(corr) << std::endl;

    return 0;
}
Overwriting tmp/mm_out_2.cpp
!g++ -I. -std=c++17 tmp/mm_out_2.cpp -o tmp/mm_out_2 \
  && ./tmp/mm_out_2
Imagem original:
 1  2  3  4 
 5  6  7  8 
 9 10 11 12 
13 14 15 16 

Kernel:
0 1 2 
0 0 0 
0 0 0 

Resultado da correlação:
 0  0  0  0 
 5  8 11  4 
17 20 23  8 
29 32 35 12 

mm::conv realiza correlação, isto é, aplica o kernel exatamente na orientação fornecida.

3.4.3.2 Convolução

%%writefile tmp/mm_out_3.cpp
#include "morph.hpp"
#include <iostream>

int main() {
    // repetidos aqui para a célula ser independente
    mm::Image img(4, 4);
    // Preenchendo a imagem com valores 1..16
    int val = 1;
    for (int y = 0; y < 4; y++) {
        for (int x = 0; x < 4; x++) {
            img.at(y, x) = val++;
        }
    }

    mm::Kernel w{{0, 1, 2},
                 {0, 0, 0},
                 {0, 0, 0}};

    // kernel rotacionado 180° (equivale a np.rot90(w, 2))
    mm::Kernel w_conv{{0, 0, 0},
                      {0, 0, 0},
                      {2, 1, 0}};

    mm::Image conv = mm::conv(img, w_conv, mm::Border::CONSTANT);

    std::cout << "Imagem original:" << "\n";
    std::cout << mm::drawImg(img) << "\n";
    std::cout << "Kernel original:" << "\n";
    std::cout << mm::drawImg(w) << "\n";
    std::cout << "Kernel rotacionado 180°:" << "\n";
    std::cout << mm::drawImg(w_conv) << "\n";
    std::cout << "Resultado da convolução:" << "\n";
    std::cout << mm::drawImg(conv) << "\n";

    return 0;
}
Overwriting tmp/mm_out_3.cpp
!g++ -I. -std=c++17 tmp/mm_out_3.cpp -o tmp/mm_out_3 \
  && ./tmp/mm_out_3
Imagem original:
 1  2  3  4 
 5  6  7  8 
 9 10 11 12 
13 14 15 16 

Kernel original:
   0    1    2 
   0    0    0 
   0    0    0 

Kernel rotacionado 180°:
   0    0    0 
   0    0    0 
   2    1    0 

Resultado da convolução:
 5 16 19 22 
 9 28 31 34 
13 40 43 46 
 0  0  0  0 

A convolução utiliza o kernel rotacionado em 180°. Para reproduzir a definição matemática de convolução, rotaciona-se o kernel (aqui, [[0,1,2],[0,0,0],[0,0,0]] → [[0,0,0],[0,0,0],[2,1,0]]) antes de aplicar mm::conv.

3.4.4 O Papel do Kernel

Os coeficientes do kernel determinam completamente o efeito produzido pelo filtro, conforme resumido na Tabela 3.2.

Tabela 3.2: Interpretação típica dos coeficientes do kernel.
Característica Efeito típico
Coeficientes positivos com soma 1 Suavização (passa-baixa)
Soma igual a 0, com valores positivos e negativos Detecção de bordas (passa-alta)
Coeficiente central positivo dominante e vizinhos negativos Realce de nitidez
Coeficientes assimétricos Gradiente direcional

Exemplos:

Suavização: \[ \frac{1}{9} \begin{bmatrix} 1&1&1\\ 1&1&1\\ 1&1&1 \end{bmatrix} \]

Detecção de bordas: \[ \begin{bmatrix} -1&-1&-1\\ -1&8&-1\\ -1&-1&-1 \end{bmatrix} \]

Realce de nitidez: \[ \begin{bmatrix} 0&-1&0\\ -1&5&-1\\ 0&-1&0 \end{bmatrix} \]

Gradiente direcional (Sobel): \[ \begin{bmatrix} -1&0&1\\ -2&0&2\\ -1&0&1 \end{bmatrix} \]

A Figura 3.10 demonstra o mecanismo passo a passo: para cada posição da janela, multiplica-se cada coeficiente do kernel pelo pixel correspondente da vizinhança e somam-se os produtos obtidos. O resultado é exatamente o valor definido pela Equação 3.11 para aquela posição da imagem. Embora as imagens produzidas por mm::conv0 e cv2.filter2D (ou mm::conv) sejam visualmente muito semelhantes, a implementação baseada em OpenCV é milhares de vezes mais rápida, como mostrado a seguir.

AvisoDesempenho: laços Python vs. operações vetorizadas

A função mm::conv0 implementa a correlação diretamente em Python por meio de laços aninhados. Embora essa abordagem seja adequada para fins didáticos, ela executa um grande número de operações e torna-se lenta para imagens maiores.

Já mm::conv utiliza cv2.filter2D, implementado em C++ e otimizado para operações matriciais. No exemplo apresentado, a versão vetorizada foi mais de 3000 vezes mais rápida que a implementação didática, produzindo um resultado visualmente equivalente.

As diferenças numéricas observadas concentram-se principalmente nas bordas da imagem. Em mm::conv0, os pixels da borda permanecem inalterados, enquanto mm::conv utiliza uma estratégia de reflexão das bordas (cv2.BORDER_REFLECT_101, padrão do cv2.filter2D).

Por isso, mm::conv0 deve ser utilizado para compreender o algoritmo, enquanto mm::conv é a opção recomendada para aplicações práticas.

%%writefile tmp/fig_03_convolucao_passo.cpp
#define MM_OUT "tmp/fig_03_convolucao_passo.png"
//| label: fig-03-convolucao-passo
//| fig-cap: "Correlação com *kernel* de média 3×3: versão didática mm::conv0 (bordas preservadas) vs. mm::conv (borda refletida). A diferença se concentra nas bordas."
//| echo: true
//| output: true

#include "morph.hpp"
#include <iostream>
#include <filesystem>

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

    mm::Kernel w_mean = mm::Kernel::mean(3);
    mm::Image img_gray = img_leop_gray;

    mm::Image img_conv0 = mm::conv0(img_gray, w_mean);   // laços, bordas preservadas
    mm::Image img_conv = mm::conv(img_gray, w_mean);    // borda refletida

    std::cout << "Correlacao no pixel central [251,251]:" << "\n";
    std::cout << "  original = " << (int)img_gray.at(251, 251) << "\n";
    std::cout << "  conv0    = " << (int)img_conv0.at(251, 251) << "\n";
    std::cout << "  conv     = " << (int)img_conv.at(251, 251) << "\n";

    mm::show({img_gray, img_conv0, img_conv},
             MM_OUT,
             {"Original", "conv0 (laços)", "conv (vetorizado)"}, 3);
    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_03_convolucao_passo_0.png");
mm::write(img_conv0, "tmp/fig_03_convolucao_passo_1.png");
mm::write(img_conv, "tmp/fig_03_convolucao_passo_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_convolucao_passo.cpp
!g++ -I. -std=c++17 tmp/fig_03_convolucao_passo.cpp -o tmp/fig_03_convolucao_passo \
  && ./tmp/fig_03_convolucao_passo \
  && test -f "tmp/fig_03_convolucao_passo.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_convolucao_passo.png"
Correlacao no pixel central [251,251]:
  original = 104
  conv0    = 102
  conv     = 102
[1] Original
[2] conv0 (laços)
[3] conv (vetorizado)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_convolucao_passo_0.png"),
            mm.read("tmp/fig_03_convolucao_passo_1.png"),
            mm.read("tmp/fig_03_convolucao_passo_2.png"),
        ],
        titles=[
            'Original',
            'conv0 (laços)',
            'conv (vetorizado)',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_convolucao_passo_0.png (ver a versao Python)")
Figura 3.10: Correlação com kernel de média 3×3: versão didática mm::conv0 (bordas preservadas) vs. mm::conv (borda refletida). A diferença se concentra nas bordas.
%%writefile tmp/mm_out_4.cpp
#include "morph.hpp"
#include <iostream>
#include <filesystem>

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

    //| echo: false
    // A partir daqui o "sujeito" dos exemplos de filtragem passa a ser o
    // leopardo (mais textura e bordas que o mandril).
    mm::Image img_gray = mm::gray(img_leop);

    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img_gray, "tmp/state/img_gray_45.png");
// [pdi:state-io:end]
return 0;
}
Overwriting tmp/mm_out_4.cpp
!g++ -I. -std=c++17 tmp/mm_out_4.cpp -o tmp/mm_out_4 \
  && ./tmp/mm_out_4

3.4.5 Exemplo Numérico: Correlação Passo a Passo

Para tornar concreto o mecanismo da Equação 3.11, considere o kernel de média 3×3 (\(a=b=1\), todos os coeficientes \(= 1/9 \approx 0{,}111\)) aplicado ao patch 5×5 extraído da imagem do leopardo. A Figura 3.11 exibe o patch com grade e destaca em amarelo a janela 3×3 centrada no pixel \([1,1]\):

%%writefile tmp/fig_03_patch.cpp
#define MM_OUT "tmp/fig_03_patch.png"
#include "morph.hpp"
#include <iostream>

//| label: fig-03-patch
//| fig-cap: "*Patch* 5×5 extraído da imagem do leopardo (posição [250:255, 250:255]). A janela amarela destaca a vizinhança 3×3 centrada no pixel [1,1] onde a correlação será calculada."
//| 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_45.png");
// [pdi:state-io:end]

    mm::Image patch = mm::crop(img_gray, 250, 255, 250, 255);
    mm::Kernel B{{1,1,1},{1,1,1},{1,1,1}};

    std::cout << "Patch 5×5 (intensidades):\n";
    std::cout << mm::drawImg(patch) << "\n";

    mm::drawImgKernel(patch, B, 1, 1, MM_OUT, 40);

    return 0;
}
Overwriting tmp/fig_03_patch.cpp
!g++ -I. -std=c++17 tmp/fig_03_patch.cpp -o tmp/fig_03_patch \
  && ./tmp/fig_03_patch \
  && test -f "tmp/fig_03_patch.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_patch.png"
Patch 5×5 (intensidades):
 94  96 103 113 115 
107 104 103 107 114 
 98 102 112 117 116 
 81  91 112 122 118 
 85  91 110 119 121 

Processando pixel (x,y)=(1,1)  |  janela do kernel 3x3
 94  96 103 113 115 
107 104 103 107 114 
 98 102 112 117 116 
 81  91 112 122 118 
 85  91 110 119 121 
try:
    mm.show(mm.read("tmp/fig_03_patch.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_patch.png (ver a versao Python)")
Figura 3.11: Patch 5×5 extraído da imagem do leopardo (posição [250:255, 250:255]). A janela amarela destaca a vizinhança 3×3 centrada no pixel [1,1] onde a correlação será calculada.

Para ilustrar o cálculo da correlação, considere o pixel na posição \([1,1]\) do patch 5×5 mostrado na Figura 3.11. Essa posição foi escolhida apenas por conveniência didática, pois possui uma vizinhança 3×3 completa ao seu redor.

Os valores dessa vizinhança correspondem à submatriz superior esquerda do patch:

\[ \text{vizinhança} = \begin{bmatrix} 91 & 95 & 108 \\ 106 & 107 & 108 \\ 102 & 103 & 107 \end{bmatrix} \]

Aplicando a Equação 3.11 com o kernel de média:

\[ g[1,1] = \frac{ 91+95+108+106+107+108+102+103+107}{9} = \frac{927}{9} = 103 \]

O resultado (103) é ligeiramente menor que o valor original do pixel central (107), pois a média incorpora vizinhos de menor intensidade, produzindo o efeito de suavização. Na prática, o algoritmo inicia o processamento em \([0,0]\) e repete esse mesmo cálculo para cada posição da imagem, deslocando a janela até cobrir todo o domínio.

3.5 Filtragem Espacial de Suavização

Os filtros de suavização (smoothing filters) atenuam variações bruscas de intensidade, reduzindo ruído e detalhes de alta frequência. São filtros passa-baixa — preservam as componentes de baixa frequência (estruturas grandes) e atenuam as de alta frequência (ruído, bordas).

3.5.1 Filtro de Média (Box Filter)

O filtro de média utiliza um kernel uniforme de tamanho \(n \times n\), onde todos os coeficientes valem \(1/n^2\):

\[ w_{\text{média}} = \frac{1}{n^2} \begin{bmatrix} 1 & \cdots & 1 \\ \vdots & \ddots & \vdots \\ 1 & \cdots & 1 \end{bmatrix}_{n \times n} \tag{3.13}\]

Cada pixel de saída é a média aritmética dos \(n^2\) pixels de sua vizinhança. Note que a soma dos coeficientes é sempre 1 — o brilho médio da imagem é preservado. Kernels maiores produzem suavização mais agressiva, mas borram progressivamente as bordas.

A Figura 3.12 mostra o efeito do filtro de média com kernels \(3\times3\), \(7\times7\) e \(15\times15\) sobre um detalhe da imagem do leopardo. Os resultados foram obtidos com mm::blur, que implementa o filtro de média por meio da função cv2.blur, equivalente à convolução da imagem com um kernel uniforme cujos coeficientes são \(h(x,y)=1/N^2\); de forma equivalente, o mesmo resultado pode ser obtido com mm::conv, calculando (\(g=f*h\)). À medida que o kernel aumenta, mais pixels contribuem para cada valor de saída, intensificando a suavização, reduzindo o ruído e tornando detalhes finos e bordas progressivamente mais borrados.

%%writefile tmp/fig_03_media.cpp
#define MM_OUT "tmp/fig_03_media.png"
// Compile: g++ -std=c++17 -o programa programa.cpp -I. -lstdc++fs

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

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

    // img_gray já está inicializado (inserido automaticamente)

    // Detalhe da região do olho
    int y0 = 580, y1 = 740, x0 = 680, x1 = 900;

    mm::Image img_gray_crop = mm::crop(img_gray, y0, y1, x0, x1);

    std::vector<int> sizes = {3, 7, 15};
    std::vector<mm::Image> imgs;
    imgs.push_back(img_gray_crop);

    for (int k : sizes) {
        imgs.push_back(mm::blur(img_gray_crop, k)); // ou
        // mm::Image kernel = mm::Kernel::mean(k);
        // imgs.push_back(mm::conv(img_gray_crop, mm::Kernel::mean(k)));
    }

    std::vector<std::string> titles;
    titles.push_back("Original");
    for (int k : sizes) {
        titles.push_back("Média " + std::to_string(k) + "×" + std::to_string(k));
    }

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

    return 0;
}
Overwriting tmp/fig_03_media.cpp
!g++ -I. -std=c++17 tmp/fig_03_media.cpp -o tmp/fig_03_media \
  && ./tmp/fig_03_media \
  && test -f "tmp/fig_03_media.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_media.png"
[1] Original
[2] Média 3×3
[3] Média 7×7
[4] Média 15×15
try:
    mm.show(mm.read("tmp/fig_03_media.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_media.png (ver a versao Python)")
Figura 3.12: Filtro de média com kernels de tamanho crescente (3×3, 7×7, 15×15). O borramento das bordas aumenta com o tamanho do kernel.

3.5.2 Filtro Gaussiano

O filtro Gaussiano pesa os pixels da vizinhança de acordo com uma função Gaussiana bidimensional:

\[ G(s,t) = \frac{1}{2\pi\sigma^2}\,e^{-\frac{s^2+t^2}{2\sigma^2}} \tag{3.14}\]

onde \(\sigma\) é o desvio padrão e controla o raio de influência. Pixels mais próximos do centro têm peso maior; pixels distantes são progressivamente ignorados.

A Figura 3.13 apresenta o kernel Gaussiano \(5\times5\) gerado para \(\sigma=1\). O kernel foi construído a partir do produto externo de dois vetores Gaussianos unidimensionais e posteriormente normalizado para que a soma de seus coeficientes seja igual a \(1\). Observa-se que os maiores pesos concentram-se no centro da matriz, decrescendo radialmente em direção às bordas. Essa distribuição faz com que os pixels centrais tenham maior influência no resultado da filtragem, contribuindo para uma suavização mais natural e com melhor preservação de bordas do que o filtro de média.

%%writefile tmp/fig_03_gauss_kernel.cpp
#define MM_OUT "tmp/fig_03_gauss_kernel.png"
#include "morph.hpp"
#include <iostream>
#include <iomanip>
#include <string>
#include <vector>

int main() {
    //| label: fig-03-gauss-kernel
    //| fig-cap: "*Kernel* Gaussiano 5×5 (σ=1): resposta ao impulso do filtro — pesos maiores no centro, decrescendo radialmente."
    //| echo: true
    //| output: true

    mm::Kernel w = mm::Kernel::gaussian(5, 1.0);   // mm::Kernel::gaussian(5, 1.0) na morph.hpp

    std::cout << "Kernel Gaussiano 5x5 (s=1), normalizado:\n";
    for (int y = 0; y < 5; y++) {
        std::cout << "  ";
        for (int x = 0; x < 5; x++) {
            if (x > 0) std::cout << "  ";
            std::cout << std::fixed << std::setprecision(4) << w.at(y, x);
        }
        std::cout << "\n";
    }
    std::cout << "Peso central [2,2] = " << std::fixed << std::setprecision(4) << w.at(2, 2) 
              << "   |   canto [0,0] = " << std::fixed << std::setprecision(4) << w.at(0, 0) << "\n";

    // Visualização: resposta do filtro Gaussiano a um impulso central
    mm::Image impulso(5, 5);
    impulso.at(2, 2) = 255;
    mm::drawImgPlt(mm::gaussian(impulso, 5, 1.0), MM_OUT, 40);

    return 0;
}
Overwriting tmp/fig_03_gauss_kernel.cpp
!g++ -I. -std=c++17 tmp/fig_03_gauss_kernel.cpp -o tmp/fig_03_gauss_kernel \
  && ./tmp/fig_03_gauss_kernel \
  && test -f "tmp/fig_03_gauss_kernel.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_gauss_kernel.png"
Kernel Gaussiano 5x5 (s=1), normalizado:
  0.0030  0.0133  0.0219  0.0133  0.0030
  0.0133  0.0596  0.0983  0.0596  0.0133
  0.0219  0.0983  0.1621  0.0983  0.0219
  0.0133  0.0596  0.0983  0.0596  0.0133
  0.0030  0.0133  0.0219  0.0133  0.0030
Peso central [2,2] = 0.1621   |   canto [0,0] = 0.0030
 3  7 11  7  3 
 7 15 25 15  7 
11 25 41 25 11 
 7 15 25 15  7 
 3  7 11  7  3 
try:
    mm.show(mm.read("tmp/fig_03_gauss_kernel.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_gauss_kernel.png (ver a versao Python)")
Figura 3.13: Kernel Gaussiano 5×5 (σ=1): resposta ao impulso do filtro — pesos maiores no centro, decrescendo radialmente.
NotaVantagem computacional da separabilidade

Considere um kernel quadrado de tamanho \(n \times n\). Se esse filtro for separável (como o Gaussiano), a convolução 2D pode ser decomposta em duas convoluções 1D: uma horizontal e outra vertical.

Nesse caso, o custo por pixel passa de aproximadamente \(O(n^2)\) operações (convolução 2D direta) para \(O(2n)\) operações (duas convoluções 1D). Assim, a complexidade é reduzida de forma significativa, tornando o processamento mais eficiente

Comparado ao filtro de média, o Gaussiano:

  • Preserva melhor as bordas — a ponderação radial suaviza sem criar transições abruptas;
  • Não introduz anéis (ringing) no domínio da frequência, pois a Gaussiana é sua própria transformada de Fourier (Capítulo 5);
  • É controlado por \(\sigma\) — aumentar \(\sigma\) equivale a aumentar o raio de suavização de forma contínua e previsível.

A Figura 3.14 compara os filtros de média e Gaussiano aplicados à imagem do leopardo usando uma janela \(9\times9\). O filtro de média foi implementado por convolução com um kernel uniforme, em que todos os \(81\) pixels da vizinhança possuem o mesmo peso (\(1/81\)), enquanto o filtro Gaussiano foi obtido com cv2.GaussianBlur, utilizando pesos definidos por uma distribuição Gaussiana. Ambos reduzem ruído e suavizam a imagem, porém o filtro Gaussiano preserva melhor as bordas e os detalhes locais, como pode ser observado na região ampliada do olho.

A Figura 3.14 compara os filtros de média e Gaussiano aplicados a um detalhe da imagem do leopardo com kernels \(9\times 9\). O filtro de média foi obtido com mm::blur, equivalente à convolução com um kernel uniforme cujos coeficientes valem \(1/81\), enquanto o filtro Gaussiano foi obtido com mm::gaussian, equivalente à convolução com um kernel gerado a partir de uma distribuição Gaussiana. Ambos promovem suavização e redução de ruído, porém o filtro Gaussiano atribui maior peso aos pixels centrais da vizinhança, preservando melhor as bordas e os detalhes locais, como pode ser observado na região ampliada do olho.

%%writefile tmp/fig_03_gauss.cpp
#define MM_OUT "tmp/fig_03_gauss.png"
#include "morph.hpp"
#include <vector>
#include <string>
#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_45.png");
// [pdi:state-io:end]

    // Filtro de média 9×9
    mm::Image img_media9 = mm::blur(img_gray, 9);
    // Filtro Gaussiano 9×9 com σ=0 (sigma automático)
    mm::Image img_gauss9 = mm::gaussian(img_gray, 9, 0);

    // Detalhe da região do olho
    mm::Image img_gray_crop = mm::crop(img_gray, 580, 740, 680, 900);
    mm::Image img_media9_crop = mm::crop(img_media9, 580, 740, 680, 900);
    mm::Image img_gauss9_crop = mm::crop(img_gauss9, 580, 740, 680, 900);

    mm::show(
        std::vector<mm::Image>{img_gray_crop, img_media9_crop, img_gauss9_crop},
        MM_OUT,
        std::vector<std::string>{"Detalhe: Original", "Média 9×9", "Gaussiano 9×9"},
        3
    );

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

// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray_crop, "tmp/fig_03_gauss_0.png");
mm::write(img_media9_crop, "tmp/fig_03_gauss_1.png");
mm::write(img_gauss9_crop, "tmp/fig_03_gauss_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_gauss.cpp
!g++ -I. -std=c++17 tmp/fig_03_gauss.cpp -o tmp/fig_03_gauss \
  && ./tmp/fig_03_gauss \
  && test -f "tmp/fig_03_gauss.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_gauss.png"
[1] Detalhe: Original
[2] Média 9×9
[3] Gaussiano 9×9
try:
    mm.show(
        [
            mm.read("tmp/fig_03_gauss_0.png"),
            mm.read("tmp/fig_03_gauss_1.png"),
            mm.read("tmp/fig_03_gauss_2.png"),
        ],
        titles=[
            'Detalhe: Original',
            'Média 9×9',
            'Gaussiano 9×9',
        ],
        cols=3,
        figsize=(12, 8),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_gauss_0.png (ver a versao Python)")
Figura 3.14: Comparação entre filtro de média e Gaussiano (kernel 9×9, σ=0). O Gaussiano preserva melhor as bordas, visível no detalhe do rosto.

3.6 Filtragem Espacial de Realce

Os filtros de realce (sharpening filters) enfatizam transições abruptas de intensidade, aumentando a nitidez e a visibilidade de bordas. São filtros passa-alta — amplificam as componentes de alta frequência (bordas, textura) e suprimem as de baixa frequência (regiões uniformes).

A intuição é simples: se subtrairmos de uma imagem sua versão suavizada (que contém apenas as baixas frequências), o que resta são as altas frequências — bordas e detalhes. Somando esse resíduo de volta à imagem original, o contraste local aumenta:

\[ g = f + k\,(f - f_{\text{suave}}), \quad k > 0 \tag{3.15}\]

O Figura 3.15 ilustra esse processo em um sinal 1D sintético com três estruturas distintas: um degrau largo, um pico fino e uma rampa suave. No painel ①, o sinal original \(f(x)\); no ②, a versão suavizada \(f_{\text{suave}}(x)\) obtida por média móvel — note como o pico fino é atenuado. O painel ③ exibe o resíduo \(f - f_{\text{suave}}\), que retém apenas as transições abruptas. Por fim, o painel ④ mostra \(g(x)\): o pico, antes atenuado, é restaurado e amplificado em relação ao original. Ajuste \(k\) e o tamanho da janela para observar o trade-off entre nitidez e amplificação de ruído.

Os filtros de realce formalizam essa ideia diretamente no kernel, sem precisar de duas etapas separadas.

🎮 Simulador: Filtragem Espacial de Realce 1D g = f + k·(f − f_suave)
k = 1.5
janela = 9
σ = 0.04
① f(x) — Sinal Original
↓ filtro passa-baixa (média móvel)
② f_suave(x) — Pico Atenuado pelo Filtro
↓ subtração: f − f_suave
③ Resíduo (f − f_suave) — Altas Frequências / Bordas
↓ soma: f + k · resíduo
④ g(x) — Sinal com Pico Realçado
Figura 3.15: Simulador: Filtragem Espacial de Realce 1D (Unsharp Masking e High-Boost)

3.6.1 Laplaciano

O Laplaciano é um operador de segunda derivada isotrópico, ou seja, responde igualmente a variações em todas as direções, ao contrário de operadores de primeira derivada, como Sobel e Prewitt, que são direcionais:

\[ \nabla^2 f = \frac{\partial^2 f}{\partial x^2} + \frac{\partial^2 f}{\partial y^2} \tag{3.16}\]

Uma propriedade importante da segunda derivada é que seu valor é próximo de zero em regiões uniformes e elevado nas transições de intensidade. Assim, ao subtrair o Laplaciano da imagem original, reforçam-se bordas e detalhes, aumentando o contraste local:

\[ g(x,y) = f(x,y) - \nabla^2 f(x,y) \tag{3.17}\]

Na forma discreta, a segunda derivada em \(x\) é aproximada por \(f(x+1,y) - 2f(x,y) + f(x-1,y)\), e analogamente em \(y\). Somando as duas direções, obtém-se o kernel \(w_4\) (4-vizinhos) ou \(w_8\) (8-vizinhos, incluindo diagonais):

\[ w_4 = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix}, \qquad w_8 = \begin{bmatrix} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \end{bmatrix} \tag{3.18}\]

NotaSoma zero e centro negativo

Ambos os kernels têm soma dos coeficientes igual a zero: em regiões uniformes, a saída é 0 — o Laplaciano não altera o brilho médio, apenas detecta variações. O centro negativo indica que o pixel é comparado com seus vizinhos: quanto mais ele se destacar (para cima ou para baixo), maior o valor absoluto do Laplaciano naquele ponto.

No exemplo a seguir, o pixel central \([1,1]=107\) possui vizinhos \(\{95, 106, 108, 103\}\). Como esses valores são próximos entre si, a região é quase uniforme e o Laplaciano retorna um valor baixo, produzindo pouco realce. Em regiões de borda, onde há diferenças maiores entre o pixel central e seus vizinhos, o Laplaciano assume valores mais elevados (positivos ou negativos), e a operação de Equação 3.17 intensifica essas transições.

A Figura 3.16 ilustra o cálculo do Laplaciano com o kernel \(w_4\) em uma vizinhança \(3\times3\) destacada dentro de um patch \(5\times5\). O exemplo mostra o valor obtido pelo operador e o correspondente pixel realçado na imagem de saída, que passa de 107 para 123.

%%writefile tmp/fig_03_laplaciano_patch.cpp
#define MM_OUT "tmp/fig_03_laplaciano_patch.png"
//| label: fig-03-laplaciano-patch
//| fig-cap: "*Kernel* Laplaciano w4 sobre o *patch* 5×5: a janela amarela destaca a vizinhança 3×3 onde o operador de segunda derivada é calculado."
//| echo: true
//| output: true

#include <iostream>
#include <vector>
#include "morph.hpp"

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

    // patch = mm.crop(img_gray, 250, 255, 250, 255)
    mm::Image patch = mm::crop(img_gray, 250, 255, 250, 255);

    // B = np.ones((3, 3), dtype=np.uint8)
    mm::Kernel B(3, 3, 1.0);

    // print("Patch 5x5 (intensidades):")
    std::cout << "Patch 5x5 (intensidades):\n";

    // print(mm.drawImg(patch))
    std::cout << mm::drawImg(patch) << "\n";

    // mm.drawImgKernel(patch, B, 1, 1, 40)
    mm::drawImgKernel(patch, B, 1, 1, MM_OUT, 40);

    return 0;
}
Overwriting tmp/fig_03_laplaciano_patch.cpp
!g++ -I. -std=c++17 tmp/fig_03_laplaciano_patch.cpp -o tmp/fig_03_laplaciano_patch \
  && ./tmp/fig_03_laplaciano_patch \
  && test -f "tmp/fig_03_laplaciano_patch.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_laplaciano_patch.png"
Patch 5x5 (intensidades):
 94  96 103 113 115 
107 104 103 107 114 
 98 102 112 117 116 
 81  91 112 122 118 
 85  91 110 119 121 

Processando pixel (x,y)=(1,1)  |  janela do kernel 3x3
 94  96 103 113 115 
107 104 103 107 114 
 98 102 112 117 116 
 81  91 112 122 118 
 85  91 110 119 121 
try:
    mm.show(mm.read("tmp/fig_03_laplaciano_patch.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_laplaciano_patch.png (ver a versao Python)")
Figura 3.16: Kernel Laplaciano w4 sobre o patch 5×5: a janela amarela destaca a vizinhança 3×3 onde o operador de segunda derivada é calculado.

A Figura 3.17 compara a aplicação dos kernels Laplacianos \(w_4\) e \(w_8\) em um recorte maior da imagem do leopardo. Para cada caso, são mostradas a resposta bruta do operador, que evidencia as bordas e transições de intensidade, e a imagem obtida após o realce por subtração do Laplaciano. Observa-se que o kernel \(w_8\), por considerar também os vizinhos diagonais, produz uma resposta mais intensa e detecta variações em mais direções, resultando em um realce ligeiramente mais acentuado.

%%writefile tmp/fig_03_laplaciano.cpp
#define MM_OUT "tmp/fig_03_laplaciano.png"
#include "morph.hpp"
#include <iostream>
#include <vector>

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

    // As variáveis img_gray_crop já estão inicializadas aqui

    // Kernel Laplaciano w4 (vizinhança 4)
    mm::Kernel w4{{0, 1, 0},
                  {1, -4, 1},
                  {0, 1, 0}};

    // Kernel Laplaciano w8 (vizinhança 8)
    mm::Kernel w8{{1, 1, 1},
                  {1, -8, 1},
                  {1, 1, 1}};

    mm::show(
        std::vector<mm::Image>{
            img_gray_crop,
            mm::laplacian_viz(img_gray_crop, w4),
            mm::laplacian(img_gray_crop, w4),
            img_gray_crop,
            mm::laplacian_viz(img_gray_crop, w8),
            mm::laplacian(img_gray_crop, w8)
        },
        MM_OUT,
        {"Original", "Laplaciano w4", "Realce w4",
         "Original", "Laplaciano w8", "Realce w8"},
        3
    );

    return 0;
}
Overwriting tmp/fig_03_laplaciano.cpp
!g++ -I. -std=c++17 tmp/fig_03_laplaciano.cpp -o tmp/fig_03_laplaciano \
  && ./tmp/fig_03_laplaciano \
  && test -f "tmp/fig_03_laplaciano.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_laplaciano.png"
[1] Original
[2] Laplaciano w4
[3] Realce w4
[4] Original
[5] Laplaciano w8
[6] Realce w8
try:
    mm.show(mm.read("tmp/fig_03_laplaciano.png"), figsize=(14, 8))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_laplaciano.png (ver a versao Python)")
Figura 3.17: Laplaciano aplicado à imagem do leopardo: resposta bruta (bordas) com w4 e w8, e imagens realçadas pela subtração do Laplaciano. w8 é mais sensível às diagonais.

3.6.2 Operador de Sobel

O operador de Sobel estima as derivadas parciais de primeira ordem nas direções horizontal e vertical. Diferente do Laplaciano (segunda derivada), o Sobel é direcional e mais robusto ao ruído, pois cada kernel combina uma derivada com uma suavização Gaussiana perpendicular:

\[ G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix} * f, \qquad G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix} * f \tag{3.19}\]

\(G_x\) detecta bordas verticais (variação na direção \(x\)); \(G_y\) detecta bordas horizontais (variação na direção \(y\)). Os pesos \(\{1,2,1\}\) na direção perpendicular correspondem à suavização Gaussiana 1D, que reduz a sensibilidade ao ruído.

NotaSobel é correlação, não convolução

Os kernels de Sobel são assimétricos — a rotação de 180° altera o resultado. cv2.Sobel implementa correlação cruzada (como cv2.filter2D). Para obter a derivada direcional correta, os sinais já estão definidos para correlação: \(G_x\) retorna valores positivos onde a intensidade cresce da esquerda para a direita.

A magnitude do gradiente combina os dois componentes, representando a força da borda independente de direção:

\[ |\nabla f| = \sqrt{G_x^2 + G_y^2} \tag{3.20}\]

E a direção do gradiente (perpendicular à borda) é:

\[ \theta = \arctan\left(\frac{G_y}{G_x}\right) \tag{3.21}\]

Para ilustrar numericamente, calcula-se \(G_x\) e \(G_y\) manualmente no pixel central \([1,1]\) do patch 5×5:

O valor reduzido de \(|{\nabla f}|\) nesse patch confirma que a região é quase uniforme, pois o gradiente assume valores elevados apenas onde há mudanças significativas de intensidade. A Figura 3.18 aplica o operador de Sobel a um recorte maior da imagem do leopardo. São apresentadas as respostas horizontal (\(G_x\)) e vertical (\(G_y\)), obtidas por convolução com os respectivos kernels de Sobel, além da magnitude \(|{\nabla f}|\), calculada a partir da combinação de ambas. Enquanto \(G_x\) destaca bordas verticais e \(G_y\) bordas horizontais, a magnitude evidencia bordas em qualquer direção.

%%writefile tmp/fig_03_sobel.cpp
#define MM_OUT "tmp/fig_03_sobel.png"
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <filesystem>

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

    // Operador de Sobel na imagem do leopardo: a magnitude |∇f| combina os
    // gradientes horizontal e vertical, revelando todas as bordas. A
    // decomposição Gx/Gy com sinal fica na trilha Python — mm::sobel devolve
    // a magnitude já com clip.
    mm::Image mag = mm::sobel(img_gray_crop);

    mm::show({img_gray_crop, mag}, MM_OUT,
             {"Original", "Magnitude |grad f| (mm.sobel)"}, 2);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray_crop, "tmp/fig_03_sobel_0.png");
mm::write(mag, "tmp/fig_03_sobel_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_sobel.cpp
!g++ -I. -std=c++17 tmp/fig_03_sobel.cpp -o tmp/fig_03_sobel \
  && ./tmp/fig_03_sobel \
  && test -f "tmp/fig_03_sobel.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_sobel.png"
[1] Original
[2] Magnitude |grad f| (mm.sobel)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_sobel_0.png"),
            mm.read("tmp/fig_03_sobel_1.png"),
        ],
        titles=[
            'Original',
            'Magnitude |grad f| (mm.sobel)',
        ],
        cols=2,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_sobel_0.png (ver a versao Python)")
Figura 3.18: Operador de Sobel na imagem do leopardo: a magnitude |∇f| combina os gradientes horizontal e vertical, revelando todas as bordas. A decomposição Gx/Gy com sinal fica na trilha Python — mm::sobel devolve a magnitude já com clip.

3.6.3 Operador de Prewitt

O operador de Prewitt é estruturalmente idêntico ao Sobel, mas substitui a ponderação Gaussiana \(\{1,2,1\}\) por pesos uniformes \(\{1,1,1\}\):

\[ G_x = \begin{bmatrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \end{bmatrix} * f, \qquad G_y = \begin{bmatrix} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \end{bmatrix} * f \tag{3.22}\]

A magnitude e a direção do gradiente seguem as mesmas equações do Sobel (Equação 3.20 e Equação 3.21). A diferença prática é que o Prewitt é ligeiramente mais sensível ao ruído — a suavização perpendicular uniforme pondera menos o pixel central da linha — mas computacionalmente mais simples. Em imagens com baixo ruído os resultados são equivalentes.

%%writefile tmp/fig_03_prewitt.cpp
#define MM_OUT "tmp/fig_03_prewitt.png"
// Compile: g++ -std=c++17 -o program program.cpp -I. -L. -lmorph

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

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

    // Pré-condição: img_gray_crop já está inicializado como mm::Image
    // (inserido automaticamente)

    // | label: fig-03-prewitt
    // | fig-cap: "Operador de Prewitt: magnitude do gradiente, comparável ao Sobel mas sem a ponderação central. mm::prewitt devolve |∇f| com clip."
    // | echo: true
    // | output: true

    mm::show(std::vector<mm::Image>{img_gray_crop, mm::prewitt(img_gray_crop)},
             MM_OUT,
             std::vector<std::string>{"Original", "Magnitude |grad f| (mm.prewitt)"},
             2);

    return 0;
}
Overwriting tmp/fig_03_prewitt.cpp
!g++ -I. -std=c++17 tmp/fig_03_prewitt.cpp -o tmp/fig_03_prewitt \
  && ./tmp/fig_03_prewitt \
  && test -f "tmp/fig_03_prewitt.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_prewitt.png"
[1] Original
[2] Magnitude |grad f| (mm.prewitt)
try:
    mm.show(mm.read("tmp/fig_03_prewitt.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_prewitt.png (ver a versao Python)")
Figura 3.19: Operador de Prewitt: magnitude do gradiente, comparável ao Sobel mas sem a ponderação central. mm::prewitt devolve |∇f| com clip.

3.6.4 Unsharp Masking (USM)

O Unsharp Masking é uma técnica clássica de realce de nitidez originária da fotografia analógica, hoje amplamente usada em software de edição de imagens. A ideia central é extrair as componentes de alta frequência da imagem (bordas e detalhes) e somá-las de volta à original com um peso \(k\):

Tabela 3.3: Etapas do Unsharp Masking.
Etapa Operação Descrição
1 \(\bar{f} = f * G_\sigma\) Suaviza com Gaussiana — retém baixas frequências
2 \(m = f - \bar{f}\) Máscara: diferença = altas frequências (bordas)
3 \(g = f + k \cdot m\) Soma ponderada da máscara à original

Substituindo a etapa 2 na etapa 3, obtém-se a expressão compacta:

\[ g = f + k\,(f - f*G_\sigma) = (1+k)\,f - k\,(f*G_\sigma) \tag{3.23}\]

O parâmetro \(k\) controla a intensidade do realce:

  • \(k = 0\): sem realce (\(g = f\));
  • \(k = 1\): USM clássico — duplica a contribuição das altas frequências;
  • \(k > 1\): High Boost Filtering — amplificação além do dobro, útil para imagens muito borradas.
AvisoAmplificação de ruído

O USM não distingue bordas de ruído — ambos são componentes de alta frequência. Para \(k\) elevado, o ruído presente na imagem é amplificado junto com as bordas. Por isso, é recomendável aplicar uma leve suavização antes do USM em imagens ruidosas, ou usar \(\sigma\) pequeno na Gaussiana.

Para ilustrar as etapas do USM, a Figura 3.20 aplica o método a um patch \(30\times30\) da imagem do leopardo, utilizando \(\sigma=1\) e \(k=1\). Inicialmente, a imagem é suavizada por um filtro Gaussiano. Em seguida, a máscara de alta frequência é obtida pela diferença entre a imagem original e a suavizada. Por fim, essa máscara é somada à imagem original, reforçando bordas e detalhes. A figura apresenta as três etapas do processo e o resultado final do realce.

%%writefile tmp/fig_03_usm2.cpp
#define MM_OUT "tmp/fig_03_usm2.png"
//| label: fig-03-usm2
//| fig-cap: "Realce por *Unsharp Masking* num *patch* do leopardo: mm::usm faz suavização Gaussiana, subtrai da original (máscara de alta frequência) e reintroduz a máscara realçada."
//| echo: true
//| output: true

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

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

    mm::Image patch = mm::crop(img_gray_crop, 35, 65, 45, 75);

    mm::Image p_suave = mm::gaussian(patch, 7, 1.0);   // suavização Gaussiana
    mm::Image p_usm   = mm::usm(patch, 1.0);           // realce completo (k = 1.0)

    mm::show(std::vector<mm::Image>{patch, p_suave, p_usm},
             MM_OUT,
             std::vector<std::string>{"Patch original", "Suavizado (σ=1)", "Realçado USM (k=1)"}, 3);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(patch, "tmp/fig_03_usm2_0.png");
mm::write(p_suave, "tmp/fig_03_usm2_1.png");
mm::write(p_usm, "tmp/fig_03_usm2_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_usm2.cpp
!g++ -I. -std=c++17 tmp/fig_03_usm2.cpp -o tmp/fig_03_usm2 \
  && ./tmp/fig_03_usm2 \
  && test -f "tmp/fig_03_usm2.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_usm2.png"
[1] Patch original
[2] Suavizado (σ=1)
[3] Realçado USM (k=1)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_usm2_0.png"),
            mm.read("tmp/fig_03_usm2_1.png"),
            mm.read("tmp/fig_03_usm2_2.png"),
        ],
        titles=[
            'Patch original',
            'Suavizado (σ=1)',
            'Realçado USM (k=1)',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_usm2_0.png (ver a versao Python)")
Figura 3.20: Realce por Unsharp Masking num patch do leopardo: mm::usm faz suavização Gaussiana, subtrai da original (máscara de alta frequência) e reintroduz a máscara realçada.

A Figura 3.21 aplica o método USM a um recorte maior da imagem do leopardo utilizando \(\sigma=1\) e diferentes valores do fator de ganho \(k\). Em todos os casos, a máscara de alta frequência é obtida pela diferença entre a imagem original e sua versão suavizada por filtro Gaussiano. O parâmetro \(k\) controla a intensidade do realce: valores menores produzem um aumento sutil de nitidez, enquanto valores maiores reforçam progressivamente bordas e detalhes. Observa-se que, para valores elevados de \(k\), surgem halos ao redor das bordas e o ruído presente na imagem passa a ser amplificado.

%%writefile tmp/fig_03_usm.cpp
#define MM_OUT "tmp/fig_03_usm.png"
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>

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

    // Imagem original e versões com Unsharp Masking com diferentes valores de k
    mm::show(
        {img_gray_crop,
         mm::usm(img_gray_crop, 0.5), mm::usm(img_gray_crop, 1.0),
         mm::usm(img_gray_crop, 3.0), mm::usm(img_gray_crop, 5.0),
         mm::usm(img_gray_crop, 8.0)},
        MM_OUT,
        {"Original", "USM k=0.5", "USM k=1.0", "USM k=3.0", "USM k=5.0", "USM k=8.0"},
        3
    );

    return 0;
}
Overwriting tmp/fig_03_usm.cpp
!g++ -I. -std=c++17 tmp/fig_03_usm.cpp -o tmp/fig_03_usm \
  && ./tmp/fig_03_usm \
  && test -f "tmp/fig_03_usm.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_usm.png"
[1] Original
[2] USM k=0.5
[3] USM k=1.0
[4] USM k=3.0
[5] USM k=5.0
[6] USM k=8.0
try:
    mm.show(mm.read("tmp/fig_03_usm.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_usm.png (ver a versao Python)")
Figura 3.21: Unsharp Masking na imagem do leopardo com σ=1 e k de 0.5 a 8.0. Para k>2 surgem halos nas bordas e o ruído de fundo aparece.

3.6.5 Detector de Canny

O Canny combina quatro etapas em sequência — suavização Gaussiana, gradiente de Sobel, supressão de não-máximos e histerese por duplo limiar — para produzir bordas finas, binárias e conectadas. Diferente de Sobel e Prewitt, o resultado não é um mapa de gradiente contínuo, mas uma máscara onde cada pixel é borda ou não.

O parâmetro central é o par de limiares \((T_{low}, T_{high})\). Pixels com gradiente acima de \(T_{high}\) são bordas certas; abaixo de \(T_{low}\), descartados. Os pixels ambíguos — entre os dois limiares — são decididos por histerese: tornam-se borda se estiverem conectados a uma borda certa, e descartados caso contrário. Isso evita tanto a perda de trechos fracos de bordas reais quanto a inclusão de ruído isolado. Uma heurística comum é \(T_{high} = 3 \times T_{low}\).

%%writefile tmp/fig_03_canny.cpp
#define MM_OUT "tmp/fig_03_canny.png"
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>

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

    // img_gray_crop já está inicializado pela linha inserida automaticamente

    //| label: fig-03-canny
    //| fig-cap: "Detector de Canny com diferentes pares de limiar: limiares baixos capturam mais bordas (inclusive ruído); limiares altos retêm apenas as bordas mais fortes."
    //| echo: true
    //| output: true

    mm::show(
        std::vector<mm::Image>{img_gray_crop,
                 mm::canny(img_gray_crop, 30,  90),
                 mm::canny(img_gray_crop, 60,  180),
                 mm::canny(img_gray_crop, 120, 240)},
        MM_OUT,
        std::vector<std::string>{"Original", "Canny (30/90)", "Canny (60/180)", "Canny (120/240)"},
        4
    );

    return 0;
}
Overwriting tmp/fig_03_canny.cpp
!g++ -I. -std=c++17 tmp/fig_03_canny.cpp -o tmp/fig_03_canny \
  && ./tmp/fig_03_canny \
  && test -f "tmp/fig_03_canny.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_canny.png"
[1] Original
[2] Canny (30/90)
[3] Canny (60/180)
[4] Canny (120/240)
try:
    mm.show(mm.read("tmp/fig_03_canny.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_canny.png (ver a versao Python)")
Figura 3.22: Detector de Canny com diferentes pares de limiar: limiares baixos capturam mais bordas (inclusive ruído); limiares altos retêm apenas as bordas mais fortes.
NotaEscolha dos limiares

Uma heurística comum é \(T_{high} = 3 \times T_{low}\). Valores típicos dependem do intervalo de gradiente da imagem — cv2.Canny aceita valores absolutos em \([0, 255]\). Para imagens com contraste variável, calcular os limiares a partir de percentis da magnitude de Sobel é mais robusto do que valores fixos.

3.7 Filtros de Ordem: Filtro da Mediana

Os filtros de ordem (order-statistic filters) substituem o pixel central pelo valor de um percentil da distribuição de intensidades da vizinhança — ao contrário dos filtros lineares, que calculam combinações ponderadas. O mais importante é o filtro da mediana.

3.7.1 Ruído Impulsivo: Sal e Pimenta

O ruído sal e pimenta (salt-and-pepper noise) substitui pixels aleatórios por valores extremos: 0 (pimenta, preto) ou 255 (sal, branco). É comum em transmissão de imagens com erros de bit e em câmeras com sensores defeituosos.

Para entender por que os filtros lineares falham, considere uma vizinhança 3×3 onde um único pixel foi corrompido para 255:

\[ \text{vizinhança} = \begin{bmatrix} 102 & 98 & 105 \\ 100 & \mathbf{255} & 97 \\ 103 & 99 & 101 \end{bmatrix} \]

Tabela 3.4: Média vs. mediana com um pixel corrompido. A mediana ignora o outlier; a média é deslocada ~40 níveis.
Método Cálculo Resultado
Média (102+98+…+255+…+101)/9 ≈ 140
Mediana {97,98,99,100,101,102,103,105,255} 101
AvisoPor que filtros de média falham com ruído impulsivo?

A média é sensível a outliers — um único pixel com valor 255 em uma vizinhança de valor ≈ 100 eleva a saída para ≈ 140, espalhando o ruído pela imagem. A mediana, por ser um estimador robusto, seleciona o valor central da distribuição ordenada, descartando naturalmente os extremos sem nenhum ajuste especial.

O exemplo a seguir ilustra o comportamento da média e da mediana na presença de um pixel corrompido por ruído impulsivo. Observa-se que a média é fortemente influenciada pelo valor extremo (255), produzindo uma estimativa distante dos valores predominantes da vizinhança. Já a mediana permanece próxima do valor original da região, evidenciando sua maior robustez a outliers e justificando seu uso na remoção de ruído sal e pimenta.

A Figura 3.23 apresenta o efeito do ruído sal e pimenta em diferentes densidades. O ruído foi gerado substituindo aleatoriamente uma fração dos pixels por valores mínimos (0, pimenta) e máximos (255, sal). À medida que a densidade aumenta de 2% para 10%, cresce a quantidade de pixels corrompidos, tornando a degradação visual mais evidente e dificultando a percepção de detalhes da imagem.

%%writefile tmp/fig_03_ruido.cpp
#define MM_OUT "tmp/fig_03_ruido.png"
#include "morph.hpp"
#include <iostream>
#include <random>
#include <filesystem>

// Função para adicionar ruído sal e pimenta
mm::Image salt_pepper(const mm::Image& img, double prob) {
    mm::Image out = img;  // cópia da imagem
    int h = out.h;
    int w = out.w;

    std::mt19937 rng(42);  // gerador com semente fixa
    std::uniform_int_distribution<int> dist_y(0, h-1);
    std::uniform_int_distribution<int> dist_x(0, w-1);
    std::uniform_real_distribution<double> dist_prob(0.0, 1.0);

    int n = static_cast<int>(prob * h * w);

    for (int i = 0; i < n; ++i) {
        int y = dist_y(rng);
        int x = dist_x(rng);
        // 50% de chance de ser sal (255) ou pimenta (0)
        out.at(y, x) = (dist_prob(rng) < 0.5) ? 0 : 255;
    }

    return out;
}

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

    // As variáveis img_gray_crop já estão inicializadas aqui

    // Aplica ruído sal e pimenta com diferentes densidades
    mm::Image n2  = salt_pepper(img_gray_crop, 0.02);
    mm::Image n5  = salt_pepper(img_gray_crop, 0.05);
    mm::Image n10 = salt_pepper(img_gray_crop, 0.10);

    // Exibe as imagens
    mm::show(std::vector<mm::Image>{img_gray_crop, n2, n5, n10},
             MM_OUT,
             std::vector<std::string>{"Original", "Ruido 2%", "Ruido 5%", "Ruido 10%"},
             4);

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray_crop, "tmp/fig_03_ruido_0.png");
mm::write(n2, "tmp/fig_03_ruido_1.png");
mm::write(n5, "tmp/fig_03_ruido_2.png");
mm::write(n10, "tmp/fig_03_ruido_3.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_ruido.cpp
!g++ -I. -std=c++17 tmp/fig_03_ruido.cpp -o tmp/fig_03_ruido \
  && ./tmp/fig_03_ruido \
  && test -f "tmp/fig_03_ruido.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_ruido.png"
[1] Original
[2] Ruido 2%
[3] Ruido 5%
[4] Ruido 10%
try:
    mm.show(
        [
            mm.read("tmp/fig_03_ruido_0.png"),
            mm.read("tmp/fig_03_ruido_1.png"),
            mm.read("tmp/fig_03_ruido_2.png"),
            mm.read("tmp/fig_03_ruido_3.png"),
        ],
        titles=[
            'Original',
            'Ruido 2%',
            'Ruido 5%',
            'Ruido 10%',
        ],
        cols=4,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_ruido_0.png (ver a versao Python)")
Figura 3.23: Ruído sal e pimenta com densidades crescentes (2%, 5%, 10%): metade dos pixels corrompidos vira sal (255), metade pimenta (0).

3.7.2 Filtro da Mediana

O filtro da mediana substitui cada pixel pelo valor mediano dos pixels da sua vizinhança \(n \times n\):

\[ g(x,y) = \text{med}_{(s,t) \in \mathcal{V}_{n}} \{f(x+s, y+t)\} \tag{3.24}\]

O valor mediano é aquele que ocupa a posição central quando os \(n^2\) valores da vizinhança são ordenados. Para uma janela \(3\times3\) (\(n^2=9\) pixels), a mediana é o 5º valor da sequência ordenada.

Para ilustrar, considere o mesmo patch 5×5 com um pixel corrompido artificialmente em \([1,1]\):

O exemplo confirma: mesmo com o pixel corrompido a 255, a mediana retorna o valor central correto — o outlier ocupa a última posição na ordenação e é descartado naturalmente.

Por ser baseada em ordenação e não em soma, a mediana possui três propriedades fundamentais que a diferenciam dos filtros lineares:

  • Robusta ao ruído impulsivo — outliers vão para as extremidades da sequência ordenada e não afetam o valor central;
  • Preservadora de bordas — transições abruptas de intensidade são mantidas, pois a mediana seleciona um valor que já existe na vizinhança, sem criar novos níveis intermediários;
  • Não linear — não pode ser expressa como convolução, portanto mm::conv não se aplica; usa-se cv2.medianBlur.

A Figura 3.24 compara diferentes técnicas de remoção de ruído sal e pimenta aplicadas a uma imagem com 10% de pixels corrompidos. Foram avaliados os filtros Gaussiano, Média, Mediana, Bilateral e Morfológico (próximo capítulo), permitindo observar o compromisso entre remoção de ruído e preservação de detalhes. Em geral, os filtros de média e Gaussiano reduzem o ruído, mas tendem a borrar as bordas, enquanto a mediana apresenta melhor desempenho para ruído impulsivo. O filtro bilateral preserva melhor as bordas, e o filtro morfológico remove boa parte dos pixels corrompidos sem degradar excessivamente a estrutura da imagem.

%%writefile tmp/fig_03_ruido_filtros.cpp
#define MM_OUT "tmp/fig_03_ruido_filtros.png"
#include "morph.hpp"
#include <random>
#include <vector>
#include <string>
#include <filesystem>

// Função para adicionar ruído sal e pimenta
mm::Image salt_pepper(const mm::Image& img, double prob) {
    mm::Image out = img;  // cópia
    int h = out.h, w = out.w;
    std::mt19937 rng(42);
    std::uniform_int_distribution<int> dist_y(0, h-1);
    std::uniform_int_distribution<int> dist_x(0, w-1);
    std::uniform_real_distribution<double> dist_prob(0.0, 1.0);

    for (int i = 0; i < static_cast<int>(prob * h * w); i++) {
        int y = dist_y(rng);
        int x = dist_x(rng);
        out.at(y, x) = (dist_prob(rng) < 0.5) ? 0 : 255;
    }
    return out;
}

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

    // img_gray_crop já está inicializado

    mm::Image noisy = salt_pepper(img_gray_crop, 0.10);

    mm::Image f_gauss   = mm::gaussian(noisy, 5, 1.0);
    mm::Image f_media   = mm::blur(noisy, 5);
    mm::Image f_median3 = mm::median(noisy, 3);
    mm::Image f_median5 = mm::median(noisy, 5);

    mm::show(
        std::vector<mm::Image>{img_gray_crop, noisy, f_gauss, f_media, f_median3, f_median5},
        MM_OUT,
        std::vector<std::string>{"Original", "Ruido 10%", "Gaussiano 5x5", "Media 5x5",
                                 "Mediana 3x3", "Mediana 5x5"},
        3
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray_crop, "tmp/fig_03_ruido_filtros_0.png");
mm::write(noisy, "tmp/fig_03_ruido_filtros_1.png");
mm::write(f_gauss, "tmp/fig_03_ruido_filtros_2.png");
mm::write(f_media, "tmp/fig_03_ruido_filtros_3.png");
mm::write(f_median3, "tmp/fig_03_ruido_filtros_4.png");
mm::write(f_median5, "tmp/fig_03_ruido_filtros_5.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_ruido_filtros.cpp
!g++ -I. -std=c++17 tmp/fig_03_ruido_filtros.cpp -o tmp/fig_03_ruido_filtros \
  && ./tmp/fig_03_ruido_filtros \
  && test -f "tmp/fig_03_ruido_filtros.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_ruido_filtros.png"
[1] Original
[2] Ruido 10%
[3] Gaussiano 5x5
[4] Media 5x5
[5] Mediana 3x3
[6] Mediana 5x5
try:
    mm.show(
        [
            mm.read("tmp/fig_03_ruido_filtros_0.png"),
            mm.read("tmp/fig_03_ruido_filtros_1.png"),
            mm.read("tmp/fig_03_ruido_filtros_2.png"),
            mm.read("tmp/fig_03_ruido_filtros_3.png"),
            mm.read("tmp/fig_03_ruido_filtros_4.png"),
            mm.read("tmp/fig_03_ruido_filtros_5.png"),
        ],
        titles=[
            'Original',
            'Ruido 10%',
            'Gaussiano 5x5',
            'Media 5x5',
            'Mediana 3x3',
            'Mediana 5x5',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_ruido_filtros_0.png (ver a versao Python)")
Figura 3.24: Filtros para ruído sal e pimenta (10%): Gaussiano, Média e Mediana. Bilateral e morfológico (open+close) ficam só na trilha Python — bilateral não tem equivalente em morph.hpp e morfologia é do próximo capítulo.

3.8 Aplicação Prática: Pré-processamento para Segmentação

Na prática, as técnicas deste capítulo raramente são usadas isoladamente. Um pipeline de pré-processamento típico combina várias etapas em sequência, adaptando-se ao tipo de imagem e à aplicação. A Figura 3.25 ilustra um pipeline completo:

  1. Equalização de histograma (CLAHE): normaliza o contraste independente das condições de iluminação;
  2. Filtro Gaussiano: suaviza ruído de aquisição sem destruir bordas;
  3. Detecção de bordas (Sobel/Canny): extrai estruturas relevantes para segmentação.
NotaOrdem importa

A ordem das operações afeta o resultado final. Em geral: (1) normalização de intensidade → (2) redução de ruído → (3) realce/segmentação. Inverter a ordem pode amplificar ruído ou perder bordas antes de detectá-las.

%%writefile tmp/fig_03_pipeline.cpp
#define MM_OUT "tmp/fig_03_pipeline.png"
// Compile: g++ -std=c++17 -o pipeline pipeline.cpp -I/path/to/morph.hpp

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

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

    // img_gray_crop já está inicializado aqui

    mm::Image img_eq = mm::equalize(img_gray_crop);    // Etapa 1: equalização global
    mm::Image img_gauss = mm::gaussian(img_eq, 5, 0);  // Etapa 2: Gaussiano
    mm::Image edges = mm::canny(img_gauss, 50, 150);   // Etapa 3: Canny

    mm::Image edges_direct = mm::canny(img_gray_crop, 50, 150);  // Canny direto, sem pré-processo

    mm::show(
        std::vector<mm::Image>{img_gray_crop, img_eq, img_gauss, edges, edges_direct},
        MM_OUT,
        std::vector<std::string>{"Original", "1. Equalizado", "2. Gaussiano", "3. Canny (pipeline)", "Canny (direto)"},
        5
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray_crop, "tmp/fig_03_pipeline_0.png");
mm::write(img_eq, "tmp/fig_03_pipeline_1.png");
mm::write(img_gauss, "tmp/fig_03_pipeline_2.png");
mm::write(edges, "tmp/fig_03_pipeline_3.png");
mm::write(edges_direct, "tmp/fig_03_pipeline_4.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_03_pipeline.cpp
!g++ -I. -std=c++17 tmp/fig_03_pipeline.cpp -o tmp/fig_03_pipeline \
  && ./tmp/fig_03_pipeline \
  && test -f "tmp/fig_03_pipeline.png" \
  || echo "⚠ mm::show não gravou tmp/fig_03_pipeline.png"
[1] Original
[2] 1. Equalizado
[3] 2. Gaussiano
[4] 3. Canny (pipeline)
[5] Canny (direto)
try:
    mm.show(
        [
            mm.read("tmp/fig_03_pipeline_0.png"),
            mm.read("tmp/fig_03_pipeline_1.png"),
            mm.read("tmp/fig_03_pipeline_2.png"),
            mm.read("tmp/fig_03_pipeline_3.png"),
            mm.read("tmp/fig_03_pipeline_4.png"),
        ],
        titles=[
            'Original',
            '1. Equalizado',
            '2. Gaussiano',
            '3. Canny (pipeline)',
            'Canny (direto)',
        ],
        cols=5,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_03_pipeline_0.png (ver a versao Python)")
Figura 3.25: Pipeline de pré-processamento: equalização → Gaussiano → Canny. A trilha Python usa CLAHE no lugar da equalização global; CLAHE não tem equivalente em morph.hpp.

3.9 Resumo

Neste capítulo foram apresentadas as principais técnicas de processamento no domínio espacial, da manipulação direta de pixels até a filtragem por vizinhança:

  • Operações de ponto: aritméticas saturadas (mm::addm, mm::subm) e lógicas bit a bit (mm::band, mm::bor, mm::bnot) para recorte de ROI e combinação de imagens; alpha blending (mm::blend) para fusão ponderada com peso \(\alpha \in [0,1]\).
  • Histograma: função discreta de distribuição de intensidades; visualizado com mm::histImg e calculado com mm::hist; base para diagnóstico tonal e para as técnicas de equalização e especificação.
  • Equalização: redistribuição automática das intensidades pela CDF (mm::equalize), com variante adaptativa CLAHE para controle local do contraste.
  • Especificação de histograma: transferência do perfil tonal de uma imagem de referência via mapeamento inverso da CDF — generalização da equalização para distribuições arbitrárias.
  • Correlação e convolução: mecanismo de janela deslizante implementado em mm::conv (cv2.filter2D); diferenciados pela rotação de 180° do kernel — relevante apenas para kernels assimétricos.
  • Filtros de suavização: média (kernel uniforme, borra bordas proporcionalmente ao tamanho) e Gaussiano (ponderação radial, separável, sem ringing, preserva melhor as bordas).
  • Filtros de realce: Laplaciano (\(w_4\)/\(w_8\), segunda derivada isotrópica), Sobel (gradiente direcional de primeira ordem, com magnitude \(|\nabla f|\) e direção \(\theta\)) e Unsharp Masking (amplificação das altas frequências com parâmetro \(k\)).
  • Filtro da mediana: não linear, robusto a outliers, preserva bordas — superior aos filtros lineares para ruído sal e pimenta.
  • Pipeline prático: encadeamento CLAHE → Gaussiano → Canny como estratégia de pré-processamento; mm::drawImgKernel para visualização didática da janela deslizante.

O Capítulo 4 abordará a morfologia matemática (erosão, dilatação, abertura e fechamento), explorando em profundidade as funções mm::ero e mm::dil da biblioteca morph.py. Em seguida, o Capítulo 5 apresentará o processamento no domínio da frequência, com foco na Transformada de Fourier e em técnicas de filtragem espectral.

3.10 🤖 Uso do Gemini Notebook como Tutor Complementar

Nesta edição, incentivamos o uso do Gemini Notebook como ferramenta complementar de aprendizagem. Essa ferramenta de IA utiliza exclusivamente os documentos fornecidos pelo autor como base de conhecimento, garantindo respostas coerentes com o conteúdo do livro — incluindo as funções da biblioteca morph.py e os experimentos realizados neste capítulo.

Para cada capítulo, preparamos um projeto específico na plataforma com o PDF do capítulo, os notebooks e materiais auxiliares. Sugerimos explorar especialmente:

  • Guia de Estudo: resumo estruturado dos conceitos, ideal para revisão antes de provas;
  • Conversa: tire dúvidas sobre equalização, convolução, filtros e pipelines diretamente com o tutor;
  • Perguntas frequentes: questões típicas sobre a diferença entre média e mediana, USM, Laplaciano vs. Sobel.
Importante🎓 Estude com o Tutor Inteligente

Para interagir com o conteúdo deste capítulo, acesse o link a seguir. O ambiente contém materiais didáticos em diferentes formatos, gerados a partir do PDF do capítulo. Na plataforma, explore especialmente as opções Guia de Estudo e Conversa para aprofundar sua compreensão.

🚀 ACESSAR Gemini Notebook: CAPÍTULO 03

🌐 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.

⚠️ Aviso sobre Conteúdo Gerado por IA

A IA é uma poderosa aliada nos estudos, mas o conteúdo gerado pode conter erros ou imprecisões. Consulte sempre livros, artigos científicos e outras fontes acadêmicas confiáveis para validar as informações. Sempre que possível, execute os exemplos práticos fornecidos neste capítulo para verificar os resultados.

3.11 Lista de Exercícios

  1. (10%) Explique a diferença entre convolução e correlação cruzada. Para quais tipos de kernel os resultados são idênticos? Dê um exemplo de kernel assimétrico (como Sobel \(G_x\)) e mostre numericamente que os resultados diferem aplicando-o ao patch 5×5 do capítulo das duas formas.

  2. (15%) Considere uma imagem 5×5 com intensidades concentradas entre os níveis 3 e 5 (baixo contraste, 3 bits). Aplique manualmente o algoritmo de equalização da Tabela 3.1, preenchendo todas as colunas da tabela (\(k\), \(h[k]\), \(p[k]\), \(\text{cdf}[k]\), \(\text{lut}[k]\)). Verifique o resultado com mm::equalize.

  3. (15%) Usando mm::conv, aplique o filtro de média com kernels de tamanho 3×3, 9×9 e 21×21 à imagem do mandrill. Para cada versão, calcule o PSNR (Peak Signal-to-Noise Ratio) em relação à original: \[\text{PSNR} = 10\log_{10}\!\left(\frac{255^2}{\text{MSE}}\right), \quad \text{MSE} = \frac{1}{MN}\sum_{i,j}(f-g)^2\] Plote o PSNR em função do tamanho do kernel e explique o que a queda progressiva indica sobre a relação entre suavização e perda de informação.

  4. (15%) Usando add_salt_pepper com densidade de 5%, aplique e compare: (a) mm::conv com média 3×3, (b) cv2.GaussianBlur com \(\sigma=1\), (c) cv2.medianBlur com janela 3×3 e (d) cv2.medianBlur com janela 5×5. Exiba as imagens com mm::show em grade 2×4 (linha 1: imagens, linha 2: histogramas via mm::histImg). Explique por que a mediana supera os filtros lineares usando o argumento da Tabela 3.4.

  5. (15%) Implemente mm::conv0 usando apenas operações NumPy vetorizadas — sem laços Python e sem cv2.filter2D — com o operador de stride tricks (np.lib.stride_tricks.sliding_window_view). Compare o resultado e o tempo de execução com mm::conv0 (laços) e mm::conv (cv2) para kernels 3×3 e 15×15 na imagem do mandrill.

  6. (15%) Aplique o Unsharp Masking com \(\sigma=1\) e \(k \in \{0.5, 1.0, 2.0, 4.0\}\) usando a função usm do capítulo. Para cada valor de \(k\): (a) calcule a diferença absoluta \(|g - f|\), (b) exiba as imagens e as diferenças com mm::show, e (c) plote o histograma das diferenças com mm::histImg. Identifique a partir de qual \(k\) os artefatos (halos e amplificação de ruído) tornam-se visualmente inaceitáveis.

  7. (15%) Escolha uma imagem de raio-X ou tomografia disponível publicamente (ex.: via mm::read de URL) e projete um pipeline de pré-processamento com pelo menos 4 etapas sequenciais, justificando cada escolha com base nos conceitos do capítulo. Exiba com mm::show em grade: imagem original, cada etapa intermediária e o resultado final com seus histogramas (mm::histImg).

Referências do Capítulo

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

  • Gonzalez (2018) para os conceitos de operações de intensidade, histograma, convolução e filtragem espacial.
  • Szeliski (2022) para a visão computacional e aplicações práticas de filtragem.
  • Bradski (2008) para a implementação prática com OpenCV e morph.py.

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

🎯 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)  # artefatos de build da trilha C++ (.cpp, binário, PNGs)

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")

# O kernel é Python mesmo na trilha C++: `mm` (morph.py) é usado pelos
# simuladores, pela exibição das figuras que o binário C++ gera e pelo
# estado mm::Image entre células. cpp=True baixa também a trilha compilada
# (morph.hpp + stb_image*.h), usada no #include das células %%writefile *.cpp.
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("EP03_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("EP03_01").run_code(codigo)

3.12.1 EP03_01 ➕ Adição Saturada de Constante

Em sistemas de vigilância por vídeo, câmeras em ambientes com iluminação variável produzem imagens subexpostas. O ajuste de brilho por adição saturada de uma constante é a operação mais simples para correção imediata, sendo aplicada em tempo real nos chips de câmeras embarcadas e em pipelines de pré-processamento de robôs móveis.

Ver na Figura 3.26 uma simulação deste EP.

3.12.1.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
  2. Constante: Ler o inteiro \(k\) (valor a ser somado).
  3. Dados: Ler os valores inteiros da matriz original linha a linha.
  4. Mapeamento: Para cada pixel \(p\), calcular o novo valor pela equação:

\[p' = \text{clip}(p + k)\]

  1. Saída: Exibir a matriz resultante com dimensões \(L \times C\).

3.12.1.2 📌 Restrições Computacionais

  • Saturação (Clipping): Os valores devem ser confinados ao intervalo \([0, 255]\): \[\text{clip}(x) = \max(0, \min(255, x))\]
  • Tipo: O resultado final deve ser inteiro (sem casas decimais).
  • \(k\) pode ser negativo: valores negativos escurecem a imagem; positivos clareiam.

3.12.1.3 🧠 Fundamentação Teórica

Parâmetro Tipo Impacto Visual
\(k > 0\) Inteiro Clareia a imagem; pixels próximos de 255 saturam em branco
\(k < 0\) Inteiro Escurece a imagem; pixels próximos de 0 saturam em preto
\(k = 0\) Inteiro Imagem inalterada

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

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linha 3: Inteiro \(k\).
  • Linhas seguintes: Elementos inteiros da matriz original.

Saída:

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

3.12.1.5 📌 Exemplos

Entrada Saída Observação
2
3
50
0 100 200
210 240 255
50 150 250
255 255 255
Saturação em 255 nos pixels altos
1
4
-30
0 20 200 255
0 0 170 225 Saturação em 0 nos pixels baixos
➕ Simulador EP03_01: Adição Saturada de Constante p' = clip(p + k)

Ajuste o valor da constante k para observar o deslocamento de brilho da imagem e o truncamento por saturação no intervalo [0, 255].

0
Entrada Original (p)
Resultado Transformado (p')
Fórmula aplicada: clip(p + (0))
Figura 3.26: Simulador EP03_01: Adição Saturada de Constante (p’ = clip(p + k))
%%writefile EP03_01.cpp
// sua solução
Overwriting EP03_01.cpp
TestSuite("EP03_01.cpp").run()
✔️ EP03_01.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_01.cases

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

3.12.2 EP03_02 🔀 Alpha Blending de Duas Imagens

Em medicina nuclear, imagens de diferentes modalidades (tomografia computadorizada e ressonância magnética) são fundidas para auxiliar no diagnóstico. A mistura ponderada (alpha blending) é a operação fundamental desse processo, permitindo ao radiologista controlar interativamente o peso de cada modalidade na imagem exibida.

Ver na Figura 3.27 uma simulação deste EP.

3.12.2.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
  2. Parâmetro: Ler o valor real \(\alpha \in [0, 1]\).
  3. Dados: Ler os valores inteiros da matriz \(f_1\) (imagem 1) e em seguida da matriz \(f_2\) (imagem 2).
  4. Mapeamento: Para cada posição \((i, j)\), calcular:

\[g(i,j) = \text{clip}\left(\text{round}\left(\alpha \cdot f_1(i,j) + (1-\alpha) \cdot f_2(i,j)\right)\right)\]

  1. Saída: Exibir a matriz resultante \(L \times C\).

3.12.2.2 📌 Restrições Computacionais

  • Arredondamento: Aplicar round antes da conversão para inteiro.
  • Saturação: Confinar ao intervalo \([0, 255]\) com \(\text{clip}(x) = \max(0, \min(255, x))\).
  • Operação em float: Realize a operação em ponto flutuante antes de arredondar.

3.12.2.3 🧠 Fundamentação Teórica

Valor de \(\alpha\) Resultado
\(\alpha = 1.0\) Apenas \(f_1\)
\(\alpha = 0.5\) Média aritmética de \(f_1\) e \(f_2\)
\(\alpha = 0.0\) Apenas \(f_2\)

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

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linha 3: Real \(\alpha\).
  • Linhas seguintes: Elementos de \(f_1\) (\(L\) linhas com \(C\) valores cada).
  • Linhas seguintes: Elementos de \(f_2\) (\(L\) linhas com \(C\) valores cada).

Saída:

  • Matriz resultante \(L \times C\).

3.12.2.5 📌 Exemplos

Entrada Saída Observação
1
3
0.5
0 100 200
100 200 50
50 150 125 Média entre as duas imagens
1
3
1.0
10 20 30
90 80 70
10 20 30 Apenas \(f_1\) (alpha=1)
🔀 Simulador EP03_02: Alpha Blending de Duas Imagens g = α·f1 + (1−α)·f2

Ajuste o parâmetro de transparência α para observar a combinação linear ponderada pixel a pixel entre as imagens f1 e f2.

0.50
α = 0.00 → Apenas f2  |  α = 0.50 → Média Ponderada Igual  |  α = 1.00 → Apenas f1
Imagem f1
Imagem f2
Resultado g
Fórmula: clip(round(0.50 · f1 + 0.50 · f2))
Figura 3.27: Simulador EP03_02: Alpha Blending de Duas Imagens (g = α·f1 + (1−α)·f2)
%%writefile EP03_02.cpp
// sua solução
Overwriting EP03_02.cpp
TestSuite("EP03_02.cpp").run()
✔️ EP03_02.cases já existe em casos/
📋 6 caso(s) carregado(s) de casos/EP03_02.cases

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

3.12.3 EP03_03 🎭 Inversão de Imagem (Negativo Fotográfico)

Em radiologia, as imagens de raio-X são tradicionalmente visualizadas em negativo: ossos aparecem em preto sobre fundo branco. A operação de negativo fotográfico é aplicada rotineiramente em PACS (Picture Archiving and Communication Systems) para facilitar a detecção de fraturas e densidades ósseas.

Ver na Figura 3.28 uma simulação deste EP.

3.12.3.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
  2. Dados: Ler os valores inteiros da matriz original.
  3. Mapeamento: Para cada pixel \(p\), calcular o negativo:

\[p' = 255 - p\]

  1. Saída: Exibir a matriz resultante \(L \times C\).

3.12.3.2 📌 Restrições Computacionais

  • Sem clipping necessário: O resultado de \(255 - p\) com \(p \in [0, 255]\) é sempre \(\in [0, 255]\).
  • Tipo inteiro: Saída deve ser valores inteiros.
  • Equivalência lógica: A operação é idêntica ao NOT bit a bit (mm::bnot) em imagens de 8 bits.

3.12.3.3 🧠 Fundamentação Teórica

Pixel Original \(p\) Pixel Negativo \(p'\) Observação
0 (preto) 255 (branco) Inversão total
128 (cinza médio) 127 (cinza médio) Valor central
255 (branco) 0 (preto) Inversão total

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

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linhas seguintes: Elementos inteiros da matriz original.

Saída:

  • Matriz negativa em \(L\) linhas e \(C\) colunas.

3.12.3.5 📌 Exemplos

Entrada Saída Observação
1
4
0 128 200 255
255 127 55 0 Inversão de cada pixel
2
2
10 20
30 40
245 235
225 215
Matriz 2x2 invertida
🎭 Simulador EP03_03: Negativo Fotográfico (Inversão) p' = 255 − p

Observe a inversão complementar de intensidade: tons escuros tornam-se claros e tons claros tornam-se escuros subtraindo cada píxel do valor máximo de 255.

Entrada Original (p)
Negativo (p' = 255 − p)
 
Fórmula aplicada: p' = 255 − p
Figura 3.28: Simulador EP03_03: Inversão de Imagem — Negativo Fotográfico (p’ = 255 − p)
%%writefile EP03_03.cpp
// sua solução
Overwriting EP03_03.cpp
TestSuite("EP03_03.cpp").run()
✔️ EP03_03.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_03.cases

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

3.12.4 EP03_04 📊 Equalização de Histograma (L bits)

Em imagens de satélite de sensoriamento remoto, a variação de iluminação ao longo do dia produz imagens de baixo contraste. A equalização de histograma é aplicada automaticamente em satélites como o Landsat para redistribuir os tons, revelando detalhes de vegetação, relevo e zonas urbanas invisíveis na imagem original.

Ver na Figura 3.29 uma simulação deste EP.

3.12.4.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas), \(C\) (colunas) e \(B\) (número de bits, com \(L_{\max} = 2^B\)).
  2. Dados: Ler a matriz de pixels \(f\) com valores em \([0, 2^B - 1]\).
  3. Histograma: Calcular \(h[k]\) = número de pixels com intensidade \(k\), para \(k = 0 \ldots 2^B-1\).
  4. Probabilidade: \(p[k] = h[k] / (L \cdot C)\).
  5. CDF: \(\text{cdf}[k] = \sum_{j=0}^{k} p[j]\); função de distribuição acumulada.
  6. LUT: \(\text{lut}[k] = \text{round}\left(\text{cdf}[k] \cdot (2^B - 1)\right)\); Look-Up Table (tabela de consulta).
  7. Aplicação: \(g[i,j] = \text{lut}[f[i,j]]\).
  8. Saída: Exibir a matriz equalizada \(L \times C\).

3.12.4.2 📌 Restrições Computacionais

  • Arredondamento: Usar arredondamento matemático (round) na LUT.
  • Bits: O número de níveis é \(2^B\) (ex.: \(B=3 \Rightarrow 8\) níveis, \(B=8 \Rightarrow 256\) níveis).
  • CDF acumulada: \(\text{cdf}[k] = \sum_{j=0}^{k} p[j]\), com \(\text{cdf}[2^B-1] = 1.0\).

3.12.4.3 🧠 Fundamentação Teórica

Etapa Operação Fórmula
1 Histograma \(h[k] \leftarrow\) nº pixels com intensidade \(k\)
2 Probabilidade \(p[k] = h[k] / (L \cdot C)\)
3 CDF \(\text{cdf}[k] = \sum_{j=0}^{k} p[j]\)
4 LUT \(\text{lut}[Look-Up Table (tabela de consulta)k] = \text{round}(\text{cdf}[k] \cdot (2^B-1))\)
5 Aplicação \(g[i,j] = \text{lut}[f[i,j]]\)

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

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linha 3: Inteiro \(B\) (número de bits).
  • Linhas seguintes: Elementos inteiros da matriz.

Saída:

  • Matriz equalizada em \(L\) linhas e \(C\) colunas.

3.12.4.5 📌 Exemplos

Entrada Saída Observação
5
5
3
3 4 2 3 4
4 3 3 4 3
2 3 4 3 2
3 4 3 2 3
4 3 2 3 4
5 7 1 5 7
7 5 5 7 5
1 5 7 5 1
5 7 5 1 5
7 5 1 5 7
Exemplo 3 bits do capítulo
1
4
3
0 0 7 7
0 0 7 7 Histograma bimodal extremo
📊 Simulador EP03_04: Equalização de Histograma lut[k] = round(cdf[k] · (L − 1))

Escolha a profundidade de bits (B) e gere imagens para analisar o espalhamento dinâmico do histograma e a tabela de remapeamento (LUT) em tempo real.

3 bits → 8 níveis
1 bit (2 níveis) 4 bits (16 níveis) 8 bits (256 níveis)
Entrada Original
Resultado Equalizado
Histograma Original
Histograma Equalizado
LUT (Tabela de Remapeamento k → v)
lut[k] = round(cdf[k] · 7) | B=3, níveis=8
Figura 3.29: Simulador EP03_04: Equalização de Histograma (Níveis L = 2^B)
%%writefile EP03_04.cpp
// sua solução
Overwriting EP03_04.cpp
TestSuite("EP03_04.cpp").run()
✔️ EP03_04.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_04.cases

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

3.12.5 EP03_05 🔲 Aplicação de Máscara AND Binária

Em sistemas de inspeção industrial por visão computacional, é necessário isolar regiões de interesse (ROI) em imagens de peças para verificar defeitos de fabricação. A operação AND bit a bit com uma máscara binária é o mecanismo fundamental para recortar exatamente a área de inspeção, zerando todos os pixels fora dela.

Ver na Figura 3.30 uma simulação deste EP.

3.12.5.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
  2. Dados: Ler a matriz de pixels \(f\) (valores \(\in [0, 255]\)).
  3. Máscara: Ler a matriz binária \(m\) (valores: apenas 0 ou 255).
  4. Mapeamento: Para cada pixel \((i,j)\), aplicar o AND bit a bit:

\[ g(i,j) = f(i,j) \;\text{AND}\; m(i,j) \]

onde \(255 =\) 11111111 e \(0 =\) 00000000 em binário.

  1. Saída: Exibir a matriz resultante \(L \times C\).

3.12.5.2 📌 Restrições Computacionais

  • AND com 255: \(p \; \text{AND} \; 255 = p\) (todos os bits preservados).
  • AND com 0: \(p \; \text{AND} \; 0 = 0\) (todos os bits zerados).
  • Máscara: Os únicos valores possíveis na máscara são 0 e 255.
  • Implementação: Em Python, o AND bit a bit entre inteiros usa o operador &.

3.12.5.3 🧠 Fundamentação Teórica

Pixel \(f\) Máscara \(m\) Resultado \(f\) AND \(m\)
qualquer \(v\) 255 (11111111) \(v\) (preservado)
qualquer \(v\) 0 (00000000) 0 (zerado)

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

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linhas seguintes: Elementos de \(f\) (\(L\) linhas).
  • Linhas seguintes: Elementos de \(m\) (\(L\) linhas com valores 0 ou 255).

Saída:

  • Matriz resultante \(L \times C\).

3.12.5.5 📌 Exemplos

Entrada Saída Observação
2
3
100 150 200
50 80 120
255 255 0
0 255 255
100 150 0
0 80 120
Máscara seleciona região
1
4
10 20 30 40
255 0 255 0
10 0 30 0 Alternado preservado/zerado
⬛ Simulador EP03_05: Máscara AND Binária g = f AND m

Clique nas células da Máscara m para alternar entre passante (255) e bloqueante (0), aplicando a operação lógica pixel a pixel.

Imagem f (0–255)
Máscara m (Clique para Alternar)
Resultado g = f AND m
—
—preservados
—zerados
—visível
Legenda:
255
Passante (preservado)
0
Bloqueante (zerado)
g(i,j) = f(i,j) & m(i,j)
Figura 3.30: Simulador EP03_05: Aplicação de Máscara AND Binária
%%writefile EP03_05.cpp
// sua solução
Overwriting EP03_05.cpp
TestSuite("EP03_05.cpp").run()
✔️ EP03_05.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_05.cases

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

3.12.6 EP03_06 🌫️ Filtro de Média com Kernel N×N

Em câmeras de veículos autônomos, imagens capturadas sob chuva ou névoa apresentam ruído gaussiano. O filtro de média é amplamente utilizado para sua redução em tempo real, sendo implementado diretamente no ISP (Image Signal Processor) de sensores CMOS (Complementary Metal-Oxide-Semiconductor).

Os sensores CMOS são os sensores de imagem usados na maioria das câmeras modernas (smartphones, webcams, câmeras automotivas etc.). Eles convertem a luz em sinais elétricos, e o ISP processa esses sinais em tempo real — aplicando operações como redução de ruído, balanço de branco e outros ajustes de imagem.

Ver na Figura 3.31 uma simulação deste EP.

3.12.6.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas), \(C\) (colunas) e \(N\) (tamanho do kernel, sempre ímpar).
  2. Dados: Ler a matriz de pixels \(f\).
  3. Filtro de Média: Para cada pixel \((i,j)\) interno (sem bordas), calcular:

\[g(i,j) = \text{round}\left(\frac{1}{N^2} \sum_{s=-(r)}^{r} \sum_{t=-(r)}^{r} f(i+s,\, j+t)\right), \quad r = \lfloor N/2 \rfloor\]

  1. Tratamento de Borda: Pixels na borda (onde a janela \(N \times N\) ultrapassa os limites) devem ser copiados diretamente do original sem modificação.
  2. Saída: Exibir a matriz resultante \(L \times C\).

3.12.6.2 📌 Restrições Computacionais

  • Raio: \(r = \lfloor N/2 \rfloor\) (metade do kernel, inteiro).
  • Pixels internos: \((i,j)\) com \(r \le i < L-r\) e \(r \le j < C-r\).
  • Arredondamento: Usar arredondamento matemático antes de converter para inteiro.
  • Sem clipping: A média de valores \(\in [0,255]\) permanece em \([0,255]\).

3.12.6.3 🧠 Fundamentação Teórica

Tamanho \(N\) Coeficiente Pixels na janela Efeito
3 \(1/9 \approx 0.111\) 9 Suave
5 \(1/25 = 0.04\) 25 Médio
7 \(1/49 \approx 0.020\) 49 Forte

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

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linha 3: Inteiro \(N\) (ímpar, \(N \ge 3\)).
  • Linhas seguintes: Elementos da matriz original.

Saída:

  • Matriz filtrada \(L \times C\).

3.12.6.5 📌 Exemplos

Entrada Saída Observação
3
3
3
10 20 30
40 50 60
70 80 90
10 20 30
40 50 60
70 80 90
Apenas borda (3×3 = borda total)
5
5
3
0 0 0 0 0
0 0 0 0 0
0 0 100 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 11 11 11 0
0 11 11 11 0
0 11 11 11 0
0 0 0 0 0
Pixel isolado: todos os 9 pixels internos cuja janela 3×3 inclui o valor 100 recebem round(100/9)=11
🔲 Simulador EP03_06: Filtro de Média com Kernel N×N g = Média(Vizinhos)

Selecione o tamanho do kernel e passe o mouse sobre os pixels do resultado para inspecionar a vizinhança e o cálculo da média aritmética.

Tamanho do kernel:
Imagem Original f (7×7) Com ruído sal e pimenta
Resultado g (Filtro Suavizado) Passe o mouse para inspecionar
Legenda:
Janela do Kernel
Borda (Copiada)
Pixel Inspecionado
Passe o mouse sobre um pixel interno do resultado para ver o cálculo da média.
Figura 3.31: Simulador EP03_06: Filtro de Média com Kernel N×N
%%writefile EP03_06.cpp
// sua solução
Overwriting EP03_06.cpp
TestSuite("EP03_06.cpp").run()
✔️ EP03_06.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_06.cases

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

3.12.7 EP03_07 🔍 Operador Laplaciano (w4) para Realce de Bordas

Em tomografias de alta resolução, a nitidez das bordas entre tecidos é crítica para diagnóstico. O operador Laplaciano é amplamente utilizado em pipelines de pré-processamento de imagens médicas para realçar automaticamente os contornos anatômicos antes da segmentação, evitando intervenção manual do radiologista.

Ver na Figura 3.32 uma simulação deste EP.

3.12.7.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
  2. Dados: Ler a matriz de pixels \(f\).
  3. Laplaciano (w4): Para cada pixel interno \((i,j)\) com \(1 \le i < L-1\), \(1 \le j < C-1\), calcular:

\[\nabla^2 f(i,j) = f(i-1,j) + f(i+1,j) + f(i,j-1) + f(i,j+1) - 4 \cdot f(i,j)\]

  1. Realce: Calcular a imagem realçada:

\[g(i,j) = \text{clip}(f(i,j) - \nabla^2 f(i,j))\]

  1. Borda: Pixels na borda são copiados diretamente: \(g(i,j) = f(i,j)\).
  2. Saída: Exibir a matriz realçada \(L \times C\).

3.12.7.2 📌 Restrições Computacionais

  • Kernel w4: \(\begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix}\) — apenas 4-vizinhos.
  • Saturação: \(\text{clip}(x) = \max(0, \min(255, x))\) aplicado ao resultado do realce.
  • Sem arredondamento: O Laplaciano usa apenas somas/subtrações de inteiros.

3.12.7.3 🧠 Fundamentação Teórica

Região \(\nabla^2 f\) Efeito do Realce
Uniforme \(\approx 0\) Sem alteração
Borda crescente \(< 0\) Pixel clareado
Borda decrescente \(> 0\) Pixel escurecido

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

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linhas seguintes: Elementos da matriz original.

Saída:

  • Matriz realçada \(L \times C\).

3.12.7.5 📌 Exemplos

Entrada Saída Observação
3
3
0 0 0
0 100 0
0 0 0
0 0 0
0 255 0
0 0 0
Pico isolado: lap=−400, g=100−(−400)=500 → clip=255
3
3
50 50 50
50 50 50
50 50 50
50 50 50
50 50 50
50 50 50
Região uniforme: Laplaciano=0, sem alteração
📐 Simulador EP03_07: Operador Laplaciano (w4) g = f ∓ ∇²f

Selecione a variante de realce e passe o mouse sobre os pixels internos do resultado para inspecionar a vizinhança de 4 pontos e a equação do Laplaciano.

Variante:
① Imagem Original f Degrau com ruído leve
② Laplaciano ∇²f Bordas detectadas (±128 shift)
③ Resultado g = f − ∇²f Passe o mouse para inspecionar
Kernel w4 (4-Vizinhos)
0
+1
0
+1
−4
+1
0
+1
0
∇²f = T + B + L + R − 4·f
Legenda:
4-Vizinhos do Kernel
Pixel Central
Borda (Copiada)
Passe o mouse sobre um pixel interno do resultado para detalhar a equação.
Figura 3.32: Simulador EP03_07: Operador Laplaciano (w4) para Realce de Bordas
%%writefile EP03_07.cpp
// sua solução
Overwriting EP03_07.cpp
TestSuite("EP03_07.cpp").run()
✔️ EP03_07.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_07.cases

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

3.12.8 EP03_08 🧭 Gradiente de Sobel: Gx e Gy

Em robôs exploradores de Marte (como o Perseverance), a detecção de obstáculos é realizada em tempo real por câmeras estereoscópicas. O operador de Sobel calcula o gradiente direcional da cena e é utilizado no algoritmo de detecção de bordas para identificar rochas, fissuras e desníveis do terreno que possam comprometer a navegação.

Ver na Figura 3.33 uma simulação deste EP.

3.12.8.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
  2. Dados: Ler a matriz \(f\).
  3. Gx e Gy: Para cada pixel interno \((i,j)\) com \(1 \le i < L-1\), \(1 \le j < C-1\):

\[G_x(i,j) = [f(i-1,j+1) + 2f(i,j+1) + f(i+1,j+1)] - [f(i-1,j-1) + 2f(i,j-1) + f(i+1,j-1)]\]

\[G_y(i,j) = [f(i+1,j-1) + 2f(i+1,j) + f(i+1,j+1)] - [f(i-1,j-1) + 2f(i-1,j) + f(i-1,j+1)]\]

  1. Magnitude: \(|\nabla f(i,j)| = \text{clip}(\text{round}(\sqrt{G_x^2 + G_y^2}))\).
  2. Borda: Pixels de borda recebem magnitude 0.
  3. Saída: Exibir a magnitude \(L \times C\).

3.12.8.2 📌 Restrições Computacionais

  • Arredondamento: Aplicar round antes de converter para inteiro.
  • Saturação: \(\text{clip}(x) = \max(0, \min(255, x))\).
  • Raiz quadrada: Usar \(\sqrt{G_x^2 + G_y^2}\) (não a aproximação \(|G_x| + |G_y|\)).

3.12.8.3 🧠 Fundamentação Teórica

Operador Detecta Coeficientes diagonais
\(G_x\) Bordas verticais \(\pm 1\)
\(G_y\) Bordas horizontais \(\pm 1\)
\(|\nabla f|\) Todas as bordas Combinado

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

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linhas seguintes: Elementos da matriz.

Saída:

  • Magnitude do gradiente, matriz \(L \times C\).

3.12.8.5 📌 Exemplos

Entrada Saída Observação
3
3
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
Imagem nula: gradiente zero
3
3
0 0 255
0 0 255
0 0 255
0 0 0
0 255 0
0 0 0
Borda vertical central: Gx alto
🧭 Simulador EP03_08: Gradiente de Sobel (Gx e Gy) |∇f| = √(Gx² + Gy²)

Analise a decomposição horizontal (Gx) e vertical (Gy) do operador de Sobel e passe o mouse sobre os pixels da magnitude para inspecionar a vizinhança 3×3.

Kernels de Sobel:
−1
0
+1
−2
0
+2
−1
0
+1
Gx
−1
−2
−1
0
0
0
+1
+2
+1
Gy
Imagem Original f Matriz 5×5 pixels
Magnitude |∇f| √(Gx² + Gy²)
Gx — Gradiente Horizontal Azul = Negativo · Branco = Zero · Azul Vivo = Positivo
Gy — Gradiente Vertical Âmbar = Negativo · Branco = Zero · Âmbar Vivo = Positivo
Legenda:
Vizinhança 3×3 Inspecionada
Pixel Central
Borda (Forçada a 0)
Passe o mouse sobre um pixel interno da magnitude para ver a decomposição Gx e Gy.
Figura 3.33: Simulador EP03_08: Gradiente de Sobel (Gx e Gy)
%%writefile EP03_08.cpp
// sua solução
Overwriting EP03_08.cpp
TestSuite("EP03_08.cpp").run()
✔️ EP03_08.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_08.cases

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

3.12.9 EP03_09 📡 Filtro da Mediana 3×3

Imagens de radar de abertura sintética (SAR) usadas em monitoramento ambiental e militar sofrem de um tipo específico de ruído chamado speckle, que possui características similares ao ruído sal e pimenta. O filtro da mediana é o método padrão de remoção desse ruído porque preserva as bordas das estruturas enquanto elimina os pontos espúrios.

Ver na Figura 3.34 uma simulação deste EP.

3.12.9.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
  2. Dados: Ler a matriz de pixels \(f\).
  3. Filtro da Mediana 3×3: Para cada pixel interno \((i,j)\) com \(1 \le i < L-1\), \(1 \le j < C-1\):
    • Coletar os 9 pixels da vizinhança \(3 \times 3\): \(\{f(i+s, j+t) : s,t \in \{-1,0,1\}\}\).
    • Ordenar os 9 valores em ordem crescente.
    • Atribuir \(g(i,j)\) ao valor central (posição índice 4, considerando índice 0).

\[g(i,j) = \text{mediana}\{f(i+s, j+t) : s,t \in \{-1,0,1\}\}\]

  1. Borda: Copiar diretamente: \(g(i,j) = f(i,j)\).
  2. Saída: Exibir a matriz filtrada \(L \times C\).

3.12.9.2 📌 Restrições Computacionais

  • Janela: Sempre \(3 \times 3 = 9\) elementos.
  • Mediana: O elemento central da sequência ordenada (índice 4 de 0 a 8).
  • Sem clipping: A mediana de valores em \([0, 255]\) permanece em \([0, 255]\).
  • Não linear: O filtro mediana não pode ser expresso como convolução linear.

3.12.9.3 🧠 Fundamentação Teórica

Ruído Filtro de Média Filtro de Mediana
Sal e pimenta (0 ou 255) Espalha o ruído Remove sem distorcer bordas
Gaussiano Reduz eficazmente Reduz parcialmente

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

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linhas seguintes: Elementos da matriz.

Saída:

  • Matriz filtrada \(L \times C\).

3.12.9.5 📌 Exemplos

Entrada Saída Observação
3
3
100 100 100
100 0 100
100 100 100
100 100 100
100 100 100
100 100 100
Ponto preto eliminado: mediana de 8×100+1×0 = 100
3
3
50 50 50
50 255 50
50 50 50
50 50 50
50 50 50
50 50 50
Ponto branco (sal) eliminado
📉 Simulador EP03_09: Filtro da Mediana 3×3 g = Mediana(Vizinhos)

Injete ruído impulsivo (sal e pimenta) e passe o mouse sobre os pixels internos do resultado para inspecionar a ordenação do vetor de vizinhança e a eliminação do ruído.

Ruído sal (255) e pimenta (0) — ~30% dos pixels internos afetados
Imagem f — Com Ruído Sal (255) e pimenta (0) visíveis
Resultado g — Sem Ruído Passe o mouse para inspecionar
Vetor de Vizinhança 3×3 — Ordenado Passe o mouse num píxel interno do resultado para visualizar
—
Legenda:
Janela 3×3
Pixel Central
Borda (Copiada)
Mediana
Ruído (Eliminado)
Passe o mouse sobre um píxel interno do resultado para ver o processo de ordenação.
Figura 3.34: Simulador EP03_09: Filtro da Mediana 3×3
%%writefile EP03_09.cpp
// sua solução
Overwriting EP03_09.cpp
TestSuite("EP03_09.cpp").run()
✔️ EP03_09.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_09.cases

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

3.12.10 EP03_10 ✨ Unsharp Masking (USM)

Em sistemas de digitalização de documentos históricos e obras de arte, a nitidez das imagens é fundamental para leitura de textos manuscritos e detalhes ornamentais. O Unsharp Masking (USM) é o algoritmo de realce de nitidez padrão utilizado em scanners profissionais e softwares como Adobe Photoshop, controlado pelo parâmetro \(k\) que determina a intensidade do realce.

Ver na Figura 3.35 uma simulação deste EP.

3.12.10.1 📋 Diretrizes de Implementação

  1. Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
  2. Parâmetro: Ler o valor real \(k\) (intensidade do realce, \(k \ge 0\)).
  3. Dados: Ler a matriz de pixels \(f\).
  4. Suavização: Calcular \(\bar{f}\) com filtro de média \(3\times3\) (apenas pixels internos; bordas mantidas):

\[\bar{f}(i,j) = \frac{1}{9} \sum_{s=-1}^{1} \sum_{t=-1}^{1} f(i+s, j+t)\]

  1. Máscara de alta frequência: \(m(i,j) = f(i,j) - \bar{f}(i,j)\).
  2. Realce USM: Para cada pixel interno:

\[g(i,j) = \text{clip}\left(\text{round}\left(f(i,j) + k \cdot m(i,j)\right)\right)\]

  1. Borda: \(g(i,j) = f(i,j)\) (cópia direta).
  2. Saída: Exibir a matriz realçada \(L \times C\).

3.12.10.2 📌 Restrições Computacionais

  • Arredondamento: Aplicar round antes do clipping.
  • Saturação: \(\text{clip}(x) = \max(0, \min(255, x))\).
  • Operações em float: Calcular \(\bar{f}\) e \(m\) em ponto flutuante antes de arredondar o resultado final.
  • \(k = 0\): Sem realce — a saída é idêntica à entrada (exceto pelas bordas).

3.12.10.3 🧠 Fundamentação Teórica

Etapa Operação Descrição
1 \(\bar{f} = f * \frac{1}{9}\mathbf{1}_{3\times3}\) Suavização (baixas frequências)
2 \(m = f - \bar{f}\) Máscara (altas frequências)
3 \(g = \text{clip}(\text{round}(f + k \cdot m))\) Realce ponderado

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

Entrada:

  • Linha 1: Inteiro \(L\).
  • Linha 2: Inteiro \(C\).
  • Linha 3: Real \(k\).
  • Linhas seguintes: Elementos da matriz original.

Saída:

  • Matriz realçada \(L \times C\).

3.12.10.5 📌 Exemplos

Entrada Saída Observação
3
3
0.0
100 100 100
100 100 100
100 100 100
100 100 100
100 100 100
100 100 100
k=0: sem realce
3
3
1.0
50 50 50
50 200 50
50 50 50
50 50 50
50 255 50
50 50 50
k=1: pixel central realçado e saturado
✨ Simulador EP03_10: Unsharp Masking (USM) g = f + k · m

Ajuste o fator de ganho k, observe o pipeline completo de realce (desfocagem, máscara de alta frequência) e passe o mouse sobre o resultado.

Fator de ganho k: k = 1.0
① Imagem Original f Matriz 5×5 pixels
② Desfocado f̄ Média 3 × 3
③ Máscara m m = f − f̄ (Altas Frequências)
④ Resultado g = f + 1.0·m Passe o mouse para inspecionar
Legenda:
Vizinhança 3×3
Pixel Central
Borda (Copiada)
Máscara Positiva/Negativa
Passe o mouse sobre um píxel interno do resultado para rastrear o pipeline completo.
Figura 3.35: Simulador EP03_10: Unsharp Masking (USM)
%%writefile EP03_10.cpp
// sua solução
Overwriting EP03_10.cpp
TestSuite("EP03_10.cpp").run()
✔️ EP03_10.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_10.cases

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