3Operaçõ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.requestos.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"ifnot 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 configconfig.setup(cpp=True)from morph import mmimport 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 handstd::filesystem::create_directories("tmp/state");mm::write(img_gray, "tmp/state/img_gray_8.png");// [pdi:state-io:end]return0;}
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"))exceptExceptionas _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).
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 handstd::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]return0;}
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"))exceptExceptionas _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 handmm::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 );return0;}
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"
try: mm.show(mm.read("tmp/fig_03_blend.png"))exceptExceptionas _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 handmm::Image img_gray = mm::_read_state("tmp/state/img_gray_8.png");// [pdi:state-io:end]// img_gray já está inicializadoint 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 handstd::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]return0;}
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)
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 handmm::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 );return0;}
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"))exceptExceptionas _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):
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\)
\(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);return0;}
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"
try: mm.show(mm.read("tmp/fig_03_equalizacao_didatica.png"))exceptExceptionas _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 handmm::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 );return0;}
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"))exceptExceptionas _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:
Calcular a CDF da imagem de entrada: \(P_r(r_k)\).
Calcular a CDF da imagem de referência: \(P_z(z_k)\).
Para cada nível \(r_k\), encontrar o nível \(z\) que minimiza \(|P_z(z) - P_r(r_k)|\).
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 handmm::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 handstd::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]return0;}
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"
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)\):
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.
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.
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:
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;return0;}
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
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 handmm::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 handstd::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]return0;}
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)
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 handmm::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 handstd::filesystem::create_directories("tmp/state");mm::write(img_gray, "tmp/state/img_gray_45.png");// [pdi:state-io:end]return0;}
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: trueint main() {// [pdi:state-io] auto-generated — do not edit by handmm::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);return0;}
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"
try: mm.show(mm.read("tmp/fig_03_patch.png"))exceptExceptionas _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:
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\):
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 handmm::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 olhoint 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);return0;}
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"))exceptExceptionas _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:
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);return0;}
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"
try: mm.show(mm.read("tmp/fig_03_gauss_kernel.png"))exceptExceptionas _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 handmm::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 handstd::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 handstd::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]return0;}
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
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 1Dg = 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:
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):
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 handmm::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);return0;}
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"
try: mm.show(mm.read("tmp/fig_03_laplaciano_patch.png"))exceptExceptionas _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.
!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))exceptExceptionas _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\) 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) é:
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 handmm::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 handstd::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]return0;}
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"
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\}\):
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 handmm::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);return0;}
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"))exceptExceptionas _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.4Unsharp 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 handmm::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 handstd::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]return0;}
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)
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 handmm::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 );return0;}
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"
try: mm.show(mm.read("tmp/fig_03_usm.png"))exceptExceptionas _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 handmm::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 );return0;}
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"
try: mm.show(mm.read("tmp/fig_03_canny.png"))exceptExceptionas _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:
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 pimentamm::Image salt_pepper(const mm::Image& img, double prob) { mm::Image out = img;// cópia da imagemint 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 handmm::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 handstd::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]return0;}
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"
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 pimentamm::Image salt_pepper(const mm::Image& img, double prob) { mm::Image out = img;// cópiaint 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 handmm::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 handstd::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]return0;}
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
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:
Equalização de histograma (CLAHE): normaliza o contraste independente das condições de iluminação;
Filtro Gaussiano: suaviza ruído de aquisição sem destruir bordas;
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.
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.
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
(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.
(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.
(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.
(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.
(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.
(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.
(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.requestos.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"ifnot 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 configconfig.setup(testsuite=True, cpp=True)from morph import mmfrom testsuite import TestSuite
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.
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.
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 Imagensg = α·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.
Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
Dados: Ler os valores inteiros da matriz original.
Mapeamento: Para cada pixel \(p\), calcular o negativo:
\[p' = 255 - p\]
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.
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.
Dimensões: Ler os inteiros \(L\) (linhas) e \(C\) (colunas).
Dados: Ler a matriz de pixels \(f\) (valores \(\in [0, 255]\)).
Máscara: Ler a matriz binária \(m\) (valores: apenas 0 ou 255).
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.
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áriag = 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.
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×Ng = 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.
📐 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 fDegrau com ruído leve
② Laplaciano ∇²fBordas detectadas (±128 shift)
③ Resultado g = f − ∇²fPasse 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.
🧭 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 fMatriz 5×5 pixels
Magnitude |∇f|√(Gx² + Gy²)
Gx — Gradiente HorizontalAzul = 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.
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×3g = 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ídoSal (255) e pimenta (0) visíveis
Resultado g — Sem RuídoPasse o mouse para inspecionar
Vetor de Vizinhança 3×3 — OrdenadoPasse 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.
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 fMatriz 5×5 pixels
② Desfocado f̄Média 3 × 3
③ Máscara mm = f − f̄ (Altas Frequências)
④ Resultado g = f + 1.0·mPasse 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.