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

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

3.1 Objetivos

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

  • Manipular intensidade e pixels: Executar operações aritméticas saturadas (mm.addm, mm.subm) e lógicas bit a bit (mm.band, mm.bor, mm.bnot) para combinação e seleção de regiões de interesse (ROI), e aplicar alpha blending (mm.blend) para fusão ponderada de imagens;
  • Processar histogramas: Interpretar o histograma como diagnóstico tonal, aplicar equalização global (mm.equalize) e adaptativa (CLAHE), e realizar especificação de histograma para transferência de perfil tonal entre imagens;
  • Compreender fundamentos espaciais: Entender vizinhança, padding de borda, e a diferença entre correlação cruzada (mm.conv) e convolução — incluindo por que kernels assimétricos como Sobel produzem resultados distintos nas duas operações;
  • Aplicar filtragem de suavização: Utilizar o filtro de média (mm.conv com kernel uniforme) e o filtro Gaussiano (cv2.GaussianBlur) 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\)) para realce isotrópico de bordas, o operador de Sobel para gradiente direcional (\(G_x\), \(G_y\), magnitude e direção), e o Unsharp Masking para amplificação controlada de alta frequência pelo parâmetro \(k\);
  • Utilizar filtros de ordem: Aplicar o filtro da mediana (cv2.medianBlur) para remoção eficaz de ruído sal e pimenta, compreendendo por que sua natureza não linear e robustez a outliers o tornam superior aos filtros lineares nesse cenário;
  • Resolver problemas práticos: Encadear técnicas em pipelines de pré-processamento (ex.: CLAHE → Gaussiano → Canny) e utilizar as funções da biblioteca morph.py (mm.conv, mm.histImg, mm.equalize, mm.drawImageKernel) 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 o módulo morph.py do repositório.

import os, importlib, urllib.request
import numpy as np
import matplotlib.pyplot as plt

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

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

version = getattr(morph, "__version__", "local_file")
print(f"✅ Ambiente pronto. Módulo 'morph' carregado (versão: {version}).")
✅ Ambiente pronto. Módulo 'morph' carregado (versão: 1.1.5).

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.

import os

# Fonte : https://commons.wikimedia.org/wiki/File:Carlos_Guilherme_Rodrigues_(76515283).jpeg
# Mapa  : https://www.google.com/maps?q=-26.204734,28.226624

base    = "https://upload.wikimedia.org/wikipedia/commons"
arquivo = "Carlos_Guilherme_Rodrigues_%2876515283%29.jpeg"
url     = f"{base}/9/9b/{arquivo}"
caminho = "imagens/mandrill-exif.jpg"

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

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

# 2. Diagnóstico de tipos, dimensões e acesso a pixels
print(f"\nTipo PIL  : {type(img_obj)}   | Dimensões (x,y): {img_obj.size}")
print(f"Tipo NumPy: {type(img_color)} | Dimensões [y,x,c]: {img_color.shape}")

# 3. Exibição
mm.show(img_color, scale=90)

Tipo PIL  : <class 'PIL.JpegImagePlugin.JpegImageFile'>   | Dimensões (x,y): (960, 540)
Tipo NumPy: <class 'numpy.ndarray'> | Dimensões [y,x,c]: (540, 960, 3)
Figura 3.1: Mandrill (Mandrillus sphinx) fotografado em ambiente natural na África do Sul. A imagem possui resolução de 500 px e contém metadados EXIF com coordenadas GPS aproximadas (-26.20473, 28.22662). 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). As funções mm.addm e mm.subm usam cv2.add e cv2.subtract, que realizam saturação automática. Para o blending, converta para float32 antes de operar e aplique np.clip(..., 0, 255).astype(np.uint8) ao final, pois a operação envolve pesos fracionários.

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

fundo = 60

img_add = mm.addm(img_gray, fundo)
img_sub = mm.subm(img_gray, fundo)

mm.show(
    [img_gray, img_add, img_sub],
    titles=["Original", "addm (+60)", "subm (−60)"],
    cols=3
)
Figura 3.2: Operações aritméticas saturadas: adição de constante (clareamento) e subtração de constante (escurecimento com saturação em 0).

3.2.3 Mistura Ponderada (Alpha Blending)

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

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

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

Para que a combinação produza um resultado coerente, é necessário alinhar previamente as regiões de interesse das imagens. Na Figura 3.4, utiliza-se um recorte do rosto do leopardo:

leo = img_gray_leo[250:-300, 100:-200]

e um recorte central da região facial do mandril:

mandrill = img_gray[100:400, 380:530]

As duas regiões são escolhidas de forma que olhos e estrutura facial permaneçam aproximadamente alinhados. Em seguida, o recorte do leopardo é redimensionado para coincidir com as dimensões do mandril antes da aplicação da mistura ponderada.

A operação é realizada em float32 para evitar problemas de overflow durante as operações aritméticas, seguida de clipping e conversão final para uint8.

import os
from PIL.ExifTags import TAGS

base    = "https://upload.wikimedia.org/wikipedia/commons"
arquivo = "Leopard_%28Panthera_pardus%29_portrait.jpg"
url     = f"{base}/9/92/{arquivo}"
caminho = "imagens/leopardo.jpg"

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

img_numpy    = np.array(img_obj)
img_gray_leo = mm.gray(img_numpy)

# 2. Extração e conversão de GPS (Tag 34853)
exif = img_obj._getexif()
if exif and (gps := exif.get(34853)):
    to_dec = lambda dms, ref: float(
        -(dms[0]+dms[1]/60+dms[2]/3600) if ref in 'SW'
        else (dms[0]+dms[1]/60+dms[2]/3600)
    )
    lat, lon = to_dec(gps[2], gps[1]), to_dec(gps[4], gps[3])
    print(f"GPS Decimal: {lat:.6f}, {lon:.6f}")
    print(f"Maps: https://www.google.com/maps/search/?api=1&query={lat},{lon}")

# 3. Diagnóstico de tipos, dimensões e acesso a pixels
print(f"\nTipo PIL  : {type(img_obj)}   | Dimensões (x,y): {img_obj.size}")
print(f"Pillow (0,0): {img_obj.getpixel((0, 0))}")
print(f"Tipo NumPy: {type(img_numpy)} | Dimensões [y,x,c]: {img_numpy.shape}")
print(f"NumPy [0,0]: {img_numpy[0, 0]}")

# 4. Exibição
mm.show(img_numpy)
GPS Decimal: -19.140200, 23.814872
Maps: https://www.google.com/maps/search/?api=1&query=-19.1402,23.814872222222224

Tipo PIL  : <class 'PIL.JpegImagePlugin.JpegImageFile'>   | Dimensões (x,y): (1242, 1324)
Pillow (0,0): (192, 134, 86)
Tipo NumPy: <class 'numpy.ndarray'> | Dimensões [y,x,c]: (1324, 1242, 3)
NumPy [0,0]: [192 134  86]
Figura 3.3: Retrato de um leopardo (Panthera pardus) em ambiente natural. Crédito: C. Brück (CC BY-SA 4.0).
leo = img_gray_leo[250:-300, 100:-200]
mandrill = img_gray[100:400, 380:530]
img_leopardo = mm.resize(leo, (mandrill.shape[1], mandrill.shape[0]), method='bilinear')

alphas = [1.0, 0.8, 0.6, 0.4, 0.2, 0.0]
imgs_blend   = []
titles_blend = []

for a in alphas:
    imgs_blend.append(mm.blend(mandrill, img_leopardo, alpha=a))
    titles_blend.append(f"α={a:.1f}")

mm.show(imgs_blend, titles=titles_blend, cols=6, figsize=(18, 16))
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.

import cv2
h, w = img_gray.shape

# Máscara circular centrada na imagem
mask_circ = np.zeros((h, w), dtype=np.uint8)
cv2.circle(mask_circ, (w//2, h//2), min(h, w)//3 - 10, 255, -1)

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

mm.show(
    [img_gray, img_and, img_or, img_not],
    titles=["Original", "AND (ROI circular)", "OR (ilumina ROI)", "NOT (negativo)"],
    cols=4
)
Figura 3.5: Operações lógicas bit a bit com máscara circular: AND (isolamento da ROI), OR (iluminação da ROI) e NOT (negativo).

3.3 Histograma de Imagens

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

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

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

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

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

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

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

img_dark = mm.subm(img_gray, 80)
img_high = mm.addm(img_gray, 80)

images = [img_gray, img_dark, img_high]
titles = ["Original", "Escurecida (−80)", "Clareada (+80)"]

def hist_title(img, t):
    H = mm.hist(img)
    n0  = int(H[0])
    n255 = int(H[255]) if len(H) > 255 else 0
    return f"Histograma — {t}\n0:{n0:,}px  255:{n255:,}px"

mm.show(
    images + [mm.histImg(img) for img in images],
    titles=titles + [hist_title(img, t) for img, t in zip(images, titles)],
    rows=2, cols=3,
    figsize=(14, 8)
)
Figura 3.6: Histogramas da imagem original, de uma versão escurecida e de uma versão clareada. Os valores na segunda linha indicam a quantidade de pixels saturados em 0 (preto) e 255 (branco).

3.3.1 Equalização de Histograma

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

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

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

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

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

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

img5 = np.array([[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]], dtype=np.uint8)
L = 8  # 3 bits: níveis 0..7

# 1. Histograma com tamanho garantido até L
h = mm.hist(img5, 3)  # B=3 → 2³=8 níveis, tamanho garantido

p   = h / h.sum()  # 2. Probabilidade de cada nível p[k] = h[k] / MN

cdf = np.cumsum(p) # 3. CDF normalizada ou
# cdf = np.zeros(len(p))
# cdf[0] = p[0]
# for k in range(1, len(p)):
#     cdf[k] = cdf[k-1] + p[k] # cdf[k] = Σ p[j], j=0..k

lut = np.round(cdf * (L - 1)).astype(np.uint8)  # 4. LUT: mapeamento para [0, L-1]

img5_eq = lut[img5] # 5. Aplica LUT pixel a pixel ou, equivalente a:
# l, c = img5.shape
# img5_eq = np.zeros((l, c), dtype=np.uint8)
# for i in range(l):
#     for j in range(c):
#         img5_eq[i, j] = lut[img5[i, j]]      # g[i,j] = lut[f[i,j]]

print("Imagem original (5×5, 3 bits):")
print(img5)
print()
header = f"{'k':>3} {'h[k]':>6} {'p[k]':>7} {'CDF[k]':>8} {'lut[k]':>7}"
print(header)
print("-" * len(header))
for k in range(L):
    print(f"{k:>3} {h[k]:>6} {p[k]:>7.4f} {cdf[k]:>8.4f} {lut[k]:>7}")
print()
print("Imagem equalizada (5×5):")
print(img5_eq)

# Conta tons distintos
tons_orig = len(np.unique(img5))
tons_eq   = len(np.unique(img5_eq))

mm.show(
    [img5, img5_eq, mm.histImg(img5, L-1), mm.histImg(img5_eq, L-1)],
    titles=[f"Original ({tons_orig} tons: {sorted(np.unique(img5).tolist())})",
            f"Equalizada ({tons_eq} tons: {sorted(np.unique(img5_eq).tolist())})",
            "Histograma — Original",
            "Histograma — Equalizada"],
    rows=2, cols=2, figsize=(5, 4), dpi=100
)
Imagem original (5×5, 3 bits):
[[3 4 2 3 4]
 [4 3 3 4 3]
 [2 3 4 3 2]
 [3 4 3 2 3]
 [4 3 2 3 4]]

  k   h[k]    p[k]   CDF[k]  lut[k]
-----------------------------------
  0      0  0.0000   0.0000       0
  1      0  0.0000   0.0000       0
  2      5  0.2000   0.2000       1
  3     12  0.4800   0.6800       5
  4      8  0.3200   1.0000       7
  5      0  0.0000   1.0000       7
  6      0  0.0000   1.0000       7
  7      0  0.0000   1.0000       7

Imagem equalizada (5×5):
[[5 7 1 5 7]
 [7 5 5 7 5]
 [1 5 7 5 1]
 [5 7 5 1 5]
 [7 5 1 5 7]]
Figura 3.7: Equalização de histograma em imagem 5×5 com 3 bits (L=8): imagem original com tons concentrados em {2,3,4}, imagem equalizada com tons redistribuídos para {1,5,7}, e os respectivos histogramas evidenciando o espalhamento das frequências.

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.

img_eq    = mm.equalize(img_gray)
clahe     = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(32, 32))
img_clahe = clahe.apply(img_gray)

images = [img_gray, img_eq, img_clahe]
titles = ["Original", "mm.equalize (CDF)", "CLAHE"]

mm.show(
    images + [mm.histImg(img) for img in images],
    titles=titles + [f"Histograma — {t}" for t in titles],
    rows=2, cols=3,
    figsize=(14, 8)
)
Figura 3.8: Equalização de histograma: mm.equalize (global via CDF) vs. CLAHE (adaptativa com limitação de contraste). Os histogramas revelam o espalhamento progressivo das intensidades.

3.3.2 Especificação de Histograma

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

O procedimento envolve três etapas:

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

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

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

img_leop_gray = mm.gray(img_numpy)

def hist_specify(src, ref):
    """Mapeia src para o perfil tonal de ref via CDF."""
    cdf_src = np.cumsum(mm.hist(src) / src.size)
    cdf_ref = np.cumsum(mm.hist(ref) / ref.size)
    lut = np.array([np.argmin(np.abs(cdf_ref - v)) for v in cdf_src], dtype=np.uint8)
    return lut[src]

img_spec = hist_specify(img_gray, img_leop_gray)

images = [img_gray, img_leop_gray, img_spec]
titles = ["Mandrill (original)", "Leopardo (referência)", "Mandrill → perfil do leopardo"]

mm.show(
    images + [mm.histImg(img) for img in images],
    titles=titles + [f"Histograma — {t}" for t in titles],
    rows=2, cols=3,
    figsize=(12, 9)
)
Figura 3.9: Especificação de histograma: mandril mapeada para o perfil tonal do leopardo. A CDF da saída aproxima a CDF de referência.

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

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

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

3.4.1 Vizinhança

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

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

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

Matematicamente, a vizinhança é definida por

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

3.4.2 Tratamento de Bordas

Pixels próximos às bordas possuem parte de sua vizinhança fora da imagem. Para aplicar filtros nessas regiões, é necessário definir como os valores externos serão obtidos. As estratégias mais comuns são:

  • Zero-padding (BORDER_CONSTANT): completa a região externa com zeros.
  • Replicação (BORDER_REPLICATE): repete os valores da borda.
  • Reflexão (BORDER_REFLECT_101): espelha os pixels vizinhos à borda.

O OpenCV utiliza, por padrão, BORDER_REFLECT_101, pois essa estratégia preserva melhor a continuidade dos níveis de cinza e reduz artefatos introduzidos pelo tratamento das bordas.

O exemplo a seguir compara os três métodos usando uma matriz 3×3. Observe como cada estratégia preenche os pixels externos necessários para que filtros 3×3 possam ser aplicados também nos cantos da imagem.

import cv2
import numpy as np

img = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
], dtype=np.uint8)

bordas = {
    "CONSTANT (zero-padding)": cv2.BORDER_CONSTANT,
    "REPLICATE": cv2.BORDER_REPLICATE,
    "REFLECT_101": cv2.BORDER_REFLECT_101
}

print("Imagem original:")
print(mm.drawImg(img))

for k in [3, 5]:
    b = k // 2

    print(f"=== Kernel {k}x{k} ===")

    for nome, tipo in bordas.items():
        pad = cv2.copyMakeBorder(
            img, b, b, b, b,
            tipo,
            value=0
        )

        print(f"{nome}")
        print(mm.drawImg(pad))
Imagem original:
1 2 3 
4 5 6 
7 8 9 

=== Kernel 3x3 ===
CONSTANT (zero-padding)
0 0 0 0 0 
0 1 2 3 0 
0 4 5 6 0 
0 7 8 9 0 
0 0 0 0 0 

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

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

=== Kernel 5x5 ===
CONSTANT (zero-padding)
0 0 0 0 0 0 0 
0 0 0 0 0 0 0 
0 0 1 2 3 0 0 
0 0 4 5 6 0 0 
0 0 7 8 9 0 0 
0 0 0 0 0 0 0 
0 0 0 0 0 0 0 

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

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

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

Na biblioteca didática morph.py, os algoritmos implementados diretamente em Python (sem utilizar OpenCV ou scikit-image) não utilizam padding. Para cada pixel, em geral, os elementos da vizinhança são examinados individualmente, e apenas os vizinhos cujas coordenadas pertencem ao domínio da imagem participam do cálculo. Assim, nas regiões próximas às bordas, a vizinhança efetiva pode conter menos pixels do que a janela especificada.

3.4.3 Correlação e Convolução

Existem dois mecanismos matematicamente relacionados:

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

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

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

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

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

3.4.4 Correlação vs. Convolução no OpenCV

cv2.filter2D implementa correlação. Para convolução verdadeira com kernel assimétrico, rotacione o kernel 180° (np.rot90(w, 2)) antes de passar para filter2D.

3.4.4.1 Correlação (OpenCV: cv2.filter2D)

import cv2
import numpy as np

# imagem 4x4
img = np.array([
    [ 1,  2,  3,  4],
    [ 5,  6,  7,  8],
    [ 9, 10, 11, 12],
    [13, 14, 15, 16]
], dtype=np.float32)

# kernel assimétrico
w = np.array([
    [0, 1, 2],
    [0, 0, 0],
    [0, 0, 0]
], dtype=np.float32)

corr = cv2.filter2D(
    img, -1, w,  # -1 indica que o tipo da saída é o mesmo da entrada
    borderType=cv2.BORDER_CONSTANT
)

print("Imagem original:")
print(mm.drawImg(img))

print("Kernel:")
print(mm.drawImg(w))

print("Resultado da correlação:")
print(mm.drawImg(corr))
Imagem original:
   1    2    3    4 
   5    6    7    8 
   9   10   11   12 
  13   14   15   16 

Kernel:
  0   1   2 
  0   0   0 
  0   0   0 

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

O método cv2.filter2D realiza correlação, isto é, aplica o kernel exatamente na orientação fornecida.

3.4.4.2 Convolução

import cv2
import numpy as np

# mesmo kernel
w_conv = np.rot90(w, 2)

conv = cv2.filter2D(
    img, -1, w_conv,
    borderType=cv2.BORDER_CONSTANT
)

print("Imagem original:")
print(mm.drawImg(img))

print("Kernel original:")
print(mm.drawImg(w))

print("Kernel rotacionado 180°:")
print(mm.drawImg(w_conv))

print("Resultado da convolução:")
print(mm.drawImg(conv))
Imagem original:
   1    2    3    4 
   5    6    7    8 
   9   10   11   12 
  13   14   15   16 

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

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

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

A convolução utiliza o kernel rotacionado em 180°. Por isso, para reproduzir a definição matemática de convolução usando cv2.filter2D, é necessário aplicar previamente np.rot90(w, 2).

Quando o kernel é simétrico (por exemplo, filtros de média ou Gaussianos), correlação e convolução produzem o mesmo resultado. A diferença aparece apenas para kernels assimétricos, como o exemplo apresentado.

3.4.5 O Papel do Kernel

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

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

Exemplos:

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

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

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

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

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

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

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

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.

import time

w_mean = np.ones((3,3), dtype=np.float32) / 9.0
img_gray = img_leop_gray
# Demonstração numérica em patch 5×5
patch = img_gray[250:255, 250:255].astype(np.float32)
roi   = patch[0:3, 0:3]
print("Patch 5×5 (intensidades):")
print(patch.astype(np.int32))
print(f"\nKernel 3×3 de média:\n{w_mean}")
print(f"\nCorrelação no pixel central [1,1]: \
      {(w_mean * roi).sum():.1f}  (original: {patch[1,1]:.0f})")

# Comparação de tempo na imagem completa
t0 = time.perf_counter()
img_conv0 = mm.conv0(img_gray, w_mean)
t_conv0 = time.perf_counter() - t0

t0 = time.perf_counter()
img_conv = mm.conv(img_gray, w_mean)
t_conv = time.perf_counter() - t0

diff = np.abs(img_conv0.astype(int) - img_conv.astype(int)).max()
print(f"\nTempos na imagem {img_gray.shape[0]}x{img_gray.shape[1]}:")
print(f"  mm.conv0 (laços Python): {t_conv0:.3f} s")
print(f"  mm.conv  (cv2.filter2D): {t_conv:.5f} s")
print(f"  Aceleração:              {t_conv0/t_conv:.0f}× mais rápido")
print(f"  Diferença máxima:        {diff} (bordas com padding diferente)")

mm.show(
    [img_gray, img_conv0, img_conv],
    titles=["Original", f"conv0 ({t_conv0:.2f}s)", f"conv ({t_conv:.4f}s)"],
    cols=3
)
Patch 5×5 (intensidades):
[[ 91  95 108 116 114]
 [106 107 108 111 114]
 [102 103 107 112 116]
 [ 83  90 111 125 123]
 [ 79  88 110 126 124]]

Kernel 3×3 de média:
[[0.11111111 0.11111111 0.11111111]
 [0.11111111 0.11111111 0.11111111]
 [0.11111111 0.11111111 0.11111111]]

Correlação no pixel central [1,1]:       103.0  (original: 107)

Tempos na imagem 1324x1242:
  mm.conv0 (laços Python): 6.950 s
  mm.conv  (cv2.filter2D): 0.00114 s
  Aceleração:              6085× mais rápido
  Diferença máxima:        39 (bordas com padding diferente)
Figura 3.10: Correlação com kernel de média 3×3: versão didática (mm.conv0) vs. vetorizada (mm.conv via cv2.filter2D). A diferença máxima de 39 ocorre nas bordas.

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

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

patch = img_gray[250:255, 250:255]
B     = np.ones((3,3), dtype=np.uint8)

print("Patch 5×5 (intensidades):")
print(mm.drawImg(patch))

mm.drawImgKernel(patch, B, x=1, y=1, scale=40.0)
Patch 5×5 (intensidades):
 91  95 108 116 114 
106 107 108 111 114 
102 103 107 112 116 
 83  90 111 125 123 
 79  88 110 126 124 
Figura 3.11: Patch 5×5 extraído da imagem do leopardo (posição [250:255, 250:255]). A janela amarela destaca a vizinhança 3×3 centrada no pixel [1,1] onde a correlação será calculada.

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

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

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

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

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

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

3.5 Filtragem Espacial de Suavização

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

3.5.1 Filtro de Média (Box Filter)

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

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

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

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

# Detalhe da região do olho
y0, y1, x0, x1 = 580, 740, 680, 900
crop = lambda img: img[y0:y1, x0:x1]

img_gray_crop = crop(img_gray)

sizes = [3, 7, 15]
imgs  = [img_gray_crop] + \
    [mm.blur(img_gray_crop, k) for k in sizes] # ou
    #[mm.conv(img_gray_crop, np.ones((k,k), dtype=np.float32)/(k*k)) for k in sizes]
titles = ["Original"] + [f"Média {k}×{k}" for k in sizes]

mm.show(imgs, titles=titles, cols=4)
Figura 3.12: Filtro de média com kernels de tamanho crescente (3×3, 7×7, 15×15). O borramento das bordas aumenta com o tamanho do kernel.

3.5.2 Filtro Gaussiano

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

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

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

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

# Gera kernel Gaussiano 5×5 via cv2
k_gauss = cv2.getGaussianKernel(5, 1)
w_gauss = k_gauss @ k_gauss.T          # @ => multiplicação matricial: G(s,t) = G(s)·G(t)
w_gauss = w_gauss / w_gauss.sum()      # garante soma = 1

print("Kernel Gaussiano 5×5 (σ=1), normalizado:")
for row in w_gauss:
    print("  " + "  ".join(f"{v:.4f}" for v in row))
print(f"\nSoma dos coeficientes: {w_gauss.sum():.6f}")
print(f"Peso central vs. canto: {w_gauss[2,2]:.4f} vs. {w_gauss[0,0]:.4f} "
      f"({w_gauss[2,2]/w_gauss[0,0]:.1f}× maior)")

mm.drawImgPlt((w_gauss * 1000).astype(np.uint8), scale=40)
Kernel Gaussiano 5×5 (σ=1), normalizado:
  0.0030  0.0133  0.0219  0.0133  0.0030
  0.0133  0.0596  0.0983  0.0596  0.0133
  0.0219  0.0983  0.1621  0.0983  0.0219
  0.0133  0.0596  0.0983  0.0596  0.0133
  0.0030  0.0133  0.0219  0.0133  0.0030

Soma dos coeficientes: 1.000000
Peso central vs. canto: 0.1621 vs. 0.0030 (54.6× maior)
Figura 3.13: Kernel Gaussiano 5×5 (σ=1): pesos normalizados, maiores no centro e decrescentes radialmente. A grade facilita a leitura de cada coeficiente.
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.

# w_media9 = np.ones((9,9), dtype=np.float32) / 81.0
# img_media9 = mm.conv(img_gray, w_media9) # ou
img_media9 = mm.blur(img_gray, 9) 
# img_gauss9 = cv2.GaussianBlur(img_gray, (9,9), 0) # ou
img_gauss9 = mm.gaussian(img_gray, 9, 0) 

# Detalhe da região do olho
y0, y1, x0, x1 = 580, 740, 680, 900
crop = lambda img: img[y0:y1, x0:x1]

img_gray_crop = crop(img_gray)

mm.show(
    [crop(img_gray),  crop(img_media9),  crop(img_gauss9)],
    titles=["Detalhe: Original", "Média 9×9", "Gaussiano 9×9"],
    cols=3, figsize=(12, 8)
)
Figura 3.14: Comparação entre filtro de média e Gaussiano (kernel 9×9, σ=0). O Gaussiano preserva melhor as bordas, visível no detalhe do rosto.

3.6 Filtragem Espacial de Realce

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

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

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

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

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

🎮 Simulador: Filtragem Espacial de Realce 1D g = f + k·(f − f_suave)
k = 1.5
janela = 9
σ = 0.04
① f(x) — sinal original
↓ filtro passa-baixa (média móvel)
② f_suave(x) — pico atenuado pelo filtro
↓ subtração: f − f_suave
③ resíduo f − f_suave — só as bordas e picos sobram
↓ soma: f + k · resíduo
④ g(x) — pico realçado (compare com ① original)
Figura 3.15: Simulador: Filtragem Espacial de Realce 1D.

3.6.1 Laplaciano

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

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

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

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

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

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

NotaSoma zero e centro negativo

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

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

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

patch  = img_gray[250:255, 250:255]
w4     = np.array([[0, 1, 0],
                   [1,-4, 1],
                   [0, 1, 0]], dtype=np.float32)
B      = np.ones((3,3), dtype=np.uint8)

p      = patch.astype(np.float32)
lap_11 = int(p[0,1] + p[1,0] - 4*p[1,1] + p[1,2] + p[2,1])
real_11 = int(np.clip(p[1,1] - lap_11, 0, 255))

print("Patch original:")
print(mm.drawImg(patch))
print(f"Laplac. w4 em  [1,1]: ", end="")
print(f"{p[0,1]:.0f} + {p[1,0]:.0f} - 4×{p[1,1]:.0f} + {p[1,2]:.0f} + {p[2,1]:.0f} = {lap_11}")
print(f"Pixel realçado [1,1]: {p[1,1]:.0f} - ({lap_11}) = {real_11}")

mm.drawImgKernel(patch, B, x=1, y=1, scale=40.0)
Patch original:
 91  95 108 116 114 
106 107 108 111 114 
102 103 107 112 116 
 83  90 111 125 123 
 79  88 110 126 124 

Laplac. w4 em  [1,1]: 95 + 106 - 4×107 + 108 + 103 = -16
Pixel realçado [1,1]: 107 - (-16) = 123
Figura 3.16: Kernel Laplaciano w4 aplicado ao 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.

w4 = np.array([[0, 1, 0],
               [1,-4, 1],
               [0, 1, 0]], dtype=np.float32)
w8 = np.array([[1, 1, 1],
               [1,-8, 1],
               [1, 1, 1]], dtype=np.float32)

mm.show(
    [img_gray_crop, mm.laplacian_viz(img_gray_crop, w4), mm.laplacian(img_gray_crop, w4),
     img_gray_crop, mm.laplacian_viz(img_gray_crop, w8), mm.laplacian(img_gray_crop, w8)],
    titles=["Original", "Laplaciano w4", "Realce w4",
            "Original", "Laplaciano w8", "Realce w8"],
    rows=2, cols=3, figsize=(14, 8)
)
Figura 3.17: Laplaciano aplicado à imagem do leopardo: resposta bruta (bordas detectadas) com w4 e w8, e imagens realçadas pela subtração do Laplaciano. w8 é mais sensível às diagonais.

3.6.2 Operador de Sobel

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

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

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

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

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

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

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

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

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

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

patch = img_gray[250:255, 250:255]
p     = patch.astype(np.float32)

# Gx no pixel [1,1]: coluna direita - coluna esquerda (ponderada)
gx = (p[0,2] + 2*p[1,2] + p[2,2]) - (p[0,0] + 2*p[1,0] + p[2,0])

# Gy no pixel [1,1]: linha inferior - linha superior (ponderada)
gy = (p[2,0] + 2*p[2,1] + p[2,2]) - (p[0,0] + 2*p[0,1] + p[0,2])

mag   = np.sqrt(gx**2 + gy**2)
theta = np.degrees(np.arctan2(gy, gx))

print("Patch 5x5:")
print(mm.drawImg(patch))
print(f"Gx em [1,1]: ({p[0,2]:.0f} + 2*{p[1,2]:.0f} + {p[2,2]:.0f}) \
      - ({p[0,0]:.0f} + 2*{p[1,0]:.0f} + {p[2,0]:.0f}) = {gx:.1f}")
print(f"Gy em [1,1]: ({p[2,0]:.0f} + 2*{p[2,1]:.0f} + {p[2,2]:.0f}) \
      - ({p[0,0]:.0f} + 2*{p[0,1]:.0f} + {p[0,2]:.0f}) = {gy:.1f}")

# Substituído o | pelo prefixo mag para não quebrar o interpretador de tabelas do Quarto
print(f"mag(Grad f)  = sqrt({gx:.1f}^2 + {gy:.1f}^2) = {mag:.1f}")
print(f"Theta        = arctan({gy:.1f}/{gx:.1f}) = {theta:.1f} graus")
Patch 5x5:
 91  95 108 116 114 
106 107 108 111 114 
102 103 107 112 116 
 83  90 111 125 123 
 79  88 110 126 124 

Gx em [1,1]: (108 + 2*108 + 107)       - (91 + 2*106 + 102) = 26.0
Gy em [1,1]: (102 + 2*103 + 107)       - (91 + 2*95 + 108) = 26.0
mag(Grad f)  = sqrt(26.0^2 + 26.0^2) = 36.8
Theta        = arctan(26.0/26.0) = 45.0 graus

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.

def norm255(img):
    mn, mx = img.min(), img.max() #+1e-9 para evitar divisão por zero
    return ((img - mn) / (mx - mn + 1e-9) * 255).astype(np.uint8)

sobelx    = cv2.Sobel(img_gray_crop, cv2.CV_64F, 1, 0, ksize=3)
sobely    = cv2.Sobel(img_gray_crop, cv2.CV_64F, 0, 1, ksize=3)
magnitude = np.sqrt(sobelx**2 + sobely**2)
direcao   = np.degrees(np.arctan2(sobely, sobelx))

# magnitude = mm.sobel(img_gray_crop) # ou, mas com clip em vez de norm255

mm.show(
    [img_gray_crop, norm255(np.abs(sobelx)), norm255(np.abs(sobely)),
     norm255(magnitude), norm255(direcao % 180)],
    titles=["Original", "Gx (bordas vert.)", "Gy (bordas horiz.)",
            "Magnitude |∇f|", "Direção θ (mod 180°)"],
    cols=5
)
Figura 3.18: Operador de Sobel na imagem do leopardo: Gx detecta bordas verticais, Gy detecta bordas horizontais, e a magnitude |∇f| combina ambos, revelando todas as bordas independente de direção.

3.6.3 Operador de Prewitt

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

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

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

Kx = np.array([[-1,0,1],[-1,0,1],[-1,0,1]], dtype=np.float32)
Ky = np.array([[-1,-1,-1],[0,0,0],[1,1,1]], dtype=np.float32)
f  = img_gray_crop.astype(np.float32)

mm.show(
    [img_gray_crop,
     norm255(np.abs(cv2.filter2D(f, -1, Kx))),
     norm255(np.abs(cv2.filter2D(f, -1, Ky))),
     mm.prewitt(img_gray_crop)],
    titles=["Original", "Gx", "Gy", "Magnitude |∇f|"],
    cols=4
)
Figura 3.19: Operador de Prewitt: Gx, Gy e magnitude — comparável ao Sobel, mas sem ponderação Gaussiana perpendicular.

3.6.4 Unsharp Masking (USM)

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

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

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

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

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

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

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

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

patch  = img_gray_crop[35:65, 45:75]
k = 1.0

p      = patch.astype(np.float32)
sigma= 1.0

# Etapa 1: suavização Gaussiana
ksize   = int(6 * sigma + 1) | 1 # operador binário garante tamanho ímpar
p_suave = cv2.GaussianBlur(p, (ksize, ksize), sigma)

# Etapa 2: máscara de alta frequência
mascara = p - p_suave

# Etapa 3: realce
p_usm = np.clip(p + k * mascara, 0, 255).astype(np.uint8)

# p_usb = mm.usm(p, k) # ou

print(f"Pixel central [1,1]: original={int(p[1,1])} suave={p_suave[1,1]:.1f}"
      f" mascara={mascara[1,1]:.1f} realcado={p_usm[1,1]}")

mm.show(
    [patch,
     p_suave.astype(np.uint8),
     np.clip(mascara + 128, 0, 255).astype(np.uint8), # tornar os valores negativos visíveis na figura
     p_usm],
    titles=["Patch original",
            f"Suavizado (σ={sigma})",
            "Máscara (alta freq., +128)",
            f"Realçado USM (k={k})"],
    cols=4, figsize=(12, 4)
)
Pixel central [1,1]: original=16 suave=20.2 mascara=-4.2 realcado=11
Figura 3.20: Realce de nitidez por Unsharp Masking na imagem do leopardo: a imagem suavizada é subtraída da original para gerar a máscara de alta frequência, que é então reintroduzida para realçar bordas e detalhes.

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.

ks = [0.5, 1.0, 3.0, 5.0, 8.0] 

imgs = [img_gray_crop] + [mm.usm(img_gray_crop, k) for k in ks]
titles = ["Original"] + [f"USM k={k}" for k in ks]

mm.show(imgs, titles=titles, cols=3, rows=2, figsize=(14, 8))
Figura 3.21: Unsharp Masking na imagem do leopardo com σ=1 e k variando de 0.5 a 8.0. Para k>2 surgem artefatos nas bordas (halos) e o ruído de fundo começa a ser visível.

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}\).

mm.show(
    [img_gray_crop,
     mm.canny(img_gray_crop, 30,  90),
     mm.canny(img_gray_crop, 60,  180),
     mm.canny(img_gray_crop, 120, 240)],
    titles=["Original", "Canny (30/90)", "Canny (60/180)", "Canny (120/240)"],
    cols=4
)
Figura 3.22: Detector de Canny com diferentes pares de limiar: limiares baixos capturam mais bordas (inclusive ruído); limiares altos retêm apenas as bordas mais fortes.
NotaEscolha dos limiares

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

3.7 Filtros de Ordem: Filtro da Mediana

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

3.7.1 Ruído Impulsivo: Sal e Pimenta

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

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

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

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

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

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

# Exemplo numérico: média vs. mediana com pixel corrompido
vizinhanca = np.array([102, 98, 105, 100, 255, 97, 103, 99, 101])
print(f"Vizinhança: {sorted([int(x) for x in vizinhanca])}")
print(f"Média:      {vizinhanca.mean():.1f}  (deslocada pelo outlier 255)")
print(f"Mediana:    {int(np.median(vizinhanca))}    (ignora o outlier)")
print(f"Valor original:  ~100")
Vizinhança: [97, 98, 99, 100, 101, 102, 103, 105, 255]
Média:      117.8  (deslocada pelo outlier 255)
Mediana:    101    (ignora o outlier)
Valor original:  ~100

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.

def add_salt_pepper(img, prob=0.05, seed=42):
    """Adiciona ruído sal e pimenta com probabilidade total prob."""
    noisy = img.copy()
    rnd   = np.random.default_rng(seed).random(img.shape)
    noisy[rnd < prob / 2]     = 0    # pimenta
    noisy[rnd > 1 - prob / 2] = 255  # sal
    n_corrompidos = int((rnd < prob/2).sum() + (rnd > 1-prob/2).sum())
    return noisy, n_corrompidos

probs = [0.02, 0.05, 0.10]
imgs_noise, titles_noise = [img_gray_crop], ["Original"]

for p in probs:
    noisy, n = add_salt_pepper(img_gray_crop, p)
    imgs_noise.append(noisy)
    titles_noise.append(f"Ruído {int(p*100)}%\n({n:,} pixels)")

mm.show(imgs_noise, titles=titles_noise, cols=4)
Figura 3.23: Ruído sal e pimenta com densidades crescentes (2%, 5%, 10%). O parâmetro prob indica a fração total de pixels corrompidos, metade sal (255) e metade pimenta (0).

3.7.2 Filtro da Mediana

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

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

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

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

patch    = img_gray[250:255, 250:255].copy()
corrompido = patch.copy()
corrompido[1, 1] = 255   # injeta pixel sal no centro

# Vizinhança 3×3 centrada em [1,1]
viz_orig = sorted(patch[0:3, 0:3].ravel().tolist())
viz_corr = sorted(corrompido[0:3, 0:3].ravel().tolist())

print("Patch original:")
print(mm.drawImg(patch))
print("\nPatch com pixel corrompido [1,1]=255:")
print(mm.drawImg(corrompido))
print(f"\nVizinhança 3×3 original (ordenada):   {viz_orig}")
print(f"Mediana original:                       {viz_orig[4]}")
print(f"\nVizinhança 3×3 corrompida (ordenada): {viz_corr}")
print(f"Mediana corrompida:                     {viz_corr[4]}  ← ignora o 255")
print(f"Média  corrompida:                  {np.mean(viz_corr):.1f}  ← distorcida pelo 255")
Patch original:
 91  95 108 116 114 
106 107 108 111 114 
102 103 107 112 116 
 83  90 111 125 123 
 79  88 110 126 124 


Patch com pixel corrompido [1,1]=255:
 91  95 108 116 114 
106 255 108 111 114 
102 103 107 112 116 
 83  90 111 125 123 
 79  88 110 126 124 


Vizinhança 3×3 original (ordenada):   [91, 95, 102, 103, 106, 107, 107, 108, 108]
Mediana original:                       106

Vizinhança 3×3 corrompida (ordenada): [91, 95, 102, 103, 106, 107, 108, 108, 255]
Mediana corrompida:                     106  ← ignora o 255
Média  corrompida:                  119.4  ← distorcida pelo 255

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.

# 10% de ruído para evidenciar diferenças entre filtros
noisy_5, _ = add_salt_pepper(img_gray_crop, prob=0.1) 

# ── Filtros ───────────────────────────────────────────────────────────────────
f_gauss   = cv2.GaussianBlur(noisy_5, (5, 5), 1.0)
f_media   = mm.conv(noisy_5, np.ones((5, 5), dtype=np.float32) / 20.0)
f_median3 = cv2.medianBlur(noisy_5, 3)
f_median5 = cv2.medianBlur(noisy_5, 5)
f_bilat   = cv2.bilateralFilter(noisy_5, d=9, sigmaColor=75, sigmaSpace=75)

# filtros morfológicos no próximo capítulo, mas já adiantados aqui para comparação
# B         = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
# f_morf    = cv2.morphologyEx(           # apresentado no próximo capítulo
#                 cv2.morphologyEx(noisy_5, cv2.MORPH_OPEN,  B),
#                 cv2.MORPH_CLOSE, B)
B = mm.secross()  # elemento estruturante de caixa 3×3
f_morf     = mm.asf(noisy_5, 'OC', B)  # equivalente via morph

# ── MSE ───────────────────────────────────────────────────────────────────────
def mse(a, b):
    return float(np.mean((a.astype(np.float32) - b.astype(np.float32))**2))

print(f"{'Filtro':<22} {'MSE':>8}")
print("-" * 32)
pares = [("Gaussiano 5×5",    f_gauss),
         ("Média 5×5",        f_media),
         ("Mediana 3×3",      f_median3),
         ("Mediana 5×5",      f_median5),
         ("Bilateral",        f_bilat),
         ("Morf. open+close", f_morf)]
for nome, img in pares:
    print(f"{nome:<22} {mse(img_gray_crop, img):>8.2f}")

imgs   = [img_gray_crop, noisy_5,    f_gauss,        f_media,
          f_median3,     f_median5,  f_bilat,        f_morf]
titles = ["Original",    "Ruído 10%", "Gaussiano 5×5","Média 5×5",
          "Mediana 3×3", "Mediana 5×5", "Bilateral", "Morf. open+close"]

mm.show(
    imgs,
    titles=[f"Crop: {t}" for t in titles],
    cols=4, rows=2, figsize=(14, 8)
)
Filtro                      MSE
--------------------------------
Gaussiano 5×5            260.84
Média 5×5                872.57
Mediana 3×3               31.26
Mediana 5×5               65.86
Bilateral               1001.22
Morf. open+close          65.16
Figura 3.24: Comparação de filtros para remoção de ruído sal e pimenta (10%): Gaussiano, Média, Mediana, Bilateral e Morfológico. Linha superior: imagens completas; linha inferior: crop da região de interesse.

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

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

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

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

# Etapa 1: CLAHE
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
img_clahe = clahe.apply(img_gray_crop)

# Etapa 2: Gaussiano
img_gauss = cv2.GaussianBlur(img_clahe, (5, 5), 0)

# Etapa 3: Canny
edges = cv2.Canny(img_gauss, 50, 150)

# Comparação: pipeline vs. Canny direto
edges_direct = cv2.Canny(img_gray_crop, 50, 150)

mm.show(
    [img_gray_crop, img_clahe, img_gauss, edges, edges_direct],
    titles=["Original", "1. CLAHE", "2. Gaussiano", "3. Canny (pipeline)", "Canny (direto)"],
    cols=5
)
Figura 3.25: Pipeline de pré-processamento: CLAHE → Gaussiano → Canny. Cada etapa prepara a imagem para a seguinte, resultando em bordas limpas e bem definidas.

3.9 Resumo

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

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

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

3.10 🤖 Uso do Gemini Notebook como Tutor Complementar

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

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

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

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

🚀 ACESSAR Gemini Notebook: CAPÍTULO 03

⚠️ Aviso sobre Conteúdo Gerado por IA

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

3.11 Lista de Exercícios

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

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

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

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

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

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

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

Referências do Capítulo

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

  • Gonzalez; Woods (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; Kaehler (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, sys, importlib, inspect, urllib.request

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

import morph, testsuite
importlib.reload(morph); importlib.reload(testsuite)
from morph import mm
from testsuite import TestSuite

print(f"\u2705 Ambiente pronto. Morph: {morph.__version__} | TestSuite: {testsuite.__version__}")
✅ Ambiente pronto. Morph: 1.1.2 | TestSuite: 1.1.2

Executando os Testes

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

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

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

3.12.1 EP03_01 ➕ Adição Saturada de Constante

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

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

3.12.1.1 📋 Diretrizes de Implementação

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

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

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

3.12.1.2 📌 Restrições Computacionais

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

3.12.1.3 🧠 Fundamentação Teórica

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

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

Entrada:

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

Saída:

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

3.12.1.5 📌 Exemplos

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

Entrada Original

Resultado (p')

Fórmula: clip(p + 0)
Figura 3.26: Simulador: Adição Saturada de Constante
%%writefile EP03_01.py
# Código Python
Writing EP03_01.py
TestSuite("EP03_01.py").run()
✔️ EP03_01.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_01.cases

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

3.12.2 EP03_02 🔀 Alpha Blending de Duas Imagens

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

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

3.12.2.1 📋 Diretrizes de Implementação

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

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

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

3.12.2.2 📌 Restrições Computacionais

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

3.12.2.3 🧠 Fundamentação Teórica

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

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

Entrada:

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

Saída:

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

3.12.2.5 📌 Exemplos

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

Imagem f1

Imagem f2

Resultado g

Fórmula: clip(round(0.5 * f1 + 0.5 * f2))
Figura 3.27: Simulador: Alpha Blending de Duas Imagens
%%writefile EP03_02.py
# Código Python
Writing EP03_02.py
TestSuite("EP03_02.py").run()
✔️ EP03_02.cases já existe em casos/
📋 6 caso(s) carregado(s) de casos/EP03_02.cases

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

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

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

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

3.12.3.1 📋 Diretrizes de Implementação

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

\[p' = 255 - p\]

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

3.12.3.2 📌 Restrições Computacionais

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

3.12.3.3 🧠 Fundamentação Teórica

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

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

Entrada:

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

Saída:

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

3.12.3.5 📌 Exemplos

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

Entrada Original

Negativo (p'=255-p)

Fórmula: p' = 255 - p
Figura 3.28: Simulador: Inversão de Imagem (Negativo Fotográfico)
%%writefile EP03_03.py
# Código Python
Writing EP03_03.py
TestSuite("EP03_03.py").run()
✔️ EP03_03.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_03.cases

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

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

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

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

3.12.4.1 📋 Diretrizes de Implementação

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

3.12.4.2 📌 Restrições Computacionais

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

3.12.4.3 🧠 Fundamentação Teórica

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

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

Entrada:

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

Saída:

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

3.12.4.5 📌 Exemplos

Entrada Saída Observação
5
5
3
3 4 2 3 4
4 3 3 4 3
2 3 4 3 2
3 4 3 2 3
4 3 2 3 4
5 7 1 5 7
7 5 5 7 5
1 5 7 5 1
5 7 5 1 5
7 5 1 5 7
Exemplo 3 bits do capítulo
1
4
3
0 0 7 7
0 0 7 7 Histograma bimodal extremo

Simulador: Equalização de Histograma

Escolha o número de bits e gere uma imagem para ver a equalização 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)

lut[k] = round( cdf[k] × 7 ) | B=3, níveis=8
Figura 3.29: Simulador: Equalização de Histograma (L bits)
%%writefile EP03_04.py
# Código Python
Writing EP03_04.py
TestSuite("EP03_04.py").run()
✔️ EP03_04.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_04.cases

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

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

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

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

3.12.5.1 📋 Diretrizes de Implementação

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

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

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

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

3.12.5.2 📌 Restrições Computacionais

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

3.12.5.3 🧠 Fundamentação Teórica

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

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

Entrada:

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

Saída:

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

3.12.5.5 📌 Exemplos

Entrada Saída Observação
2
3
100 150 200
50 80 120
255 255 0
0 255 255
100 150 0
0 80 120
Máscara seleciona região
1
4
10 20 30 40
255 0 255 0
10 0 30 0 Alternado preservado/zerado

Simulador de máscara AND binária: aplica operação AND pixel a pixel entre imagem e máscara interativa.

Imagem f

valores 0–255

Máscara m

clique para alternar

Resultado g

g = f AND m

preservados
zerados
% da imagem visível
Legenda da máscara
255
Passante — pixel preservado
0
Bloqueante — pixel zerado
g(i,j) = f(i,j) & m(i,j)
Figura 3.30: Simulador: Aplicação de Máscara AND Binária
%%writefile EP03_05.py
# Código Python
Writing EP03_05.py
TestSuite("EP03_05.py").run()
✔️ EP03_05.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_05.cases

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

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

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

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

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

3.12.6.1 📋 Diretrizes de Implementação

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

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

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

3.12.6.2 📌 Restrições Computacionais

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

3.12.6.3 🧠 Fundamentação Teórica

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

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

Entrada:

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

Saída:

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

3.12.6.5 📌 Exemplos

Entrada Saída Observação
3
3
3
10 20 30
40 50 60
70 80 90
10 20 30
40 50 60
70 80 90
Apenas borda (3×3 = borda total)
5
5
3
0 0 0 0 0
0 0 0 0 0
0 0 100 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 11 11 11 0
0 11 11 11 0
0 11 11 11 0
0 0 0 0 0
Pixel isolado: todos os 9 pixels internos cuja janela 3×3 inclui o valor 100 recebem round(100/9)=11

Simulador de filtro de média com kernel 3×3 ou 5×5: passe o mouse nas células do resultado para visualizar a janela de vizinhança.

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 (vizinhos)
Borda — copiada sem filtro
Pixel inspecionado
Passe o mouse sobre um pixel interno do resultado para ver o cálculo.
Figura 3.31: Simulador: Filtro de Média com Kernel N×N
%%writefile EP03_06.py
# Código Python
Writing EP03_06.py
TestSuite("EP03_06.py").run()
✔️ EP03_06.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_06.cases

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

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

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

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

3.12.7.1 📋 Diretrizes de Implementação

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

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

  1. Realce: Calcular a imagem realçada:

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

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

3.12.7.2 📌 Restrições Computacionais

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

3.12.7.3 🧠 Fundamentação Teórica

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

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

Entrada:

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

Saída:

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

3.12.7.5 📌 Exemplos

Entrada Saída Observação
3
3
0 0 0
0 100 0
0 0 0
0 0 0
0 255 0
0 0 0
Pico isolado: lap=−400, g=100−(−400)=500 → clip=255
3
3
50 50 50
50 50 50
50 50 50
50 50 50
50 50 50
50 50 50
Região uniforme: Laplaciano=0, sem alteração

Simulador do operador Laplaciano para realce de bordas: imagem original, laplaciano e resultado.

Variante:

① Imagem original f

degrau com ruído leve

③ Resultado g

g = f − ∇²f

② Laplaciano ∇²f

bordas detectadas (±128 shift)

Kernel w4
0
+1
0
+1
−4
+1
0
+1
0
∇²f = T+B+L+R − 4·f
Legenda
4-vizinhos do kernel
Pixel central inspecionado
Borda — copiada sem filtro
Passe o mouse sobre um pixel interno do resultado para detalhar a equação.
Figura 3.32: Simulador: Operador Laplaciano (w4) para Realce de Bordas
%%writefile EP03_07.py
# Código Python
Writing EP03_07.py
TestSuite("EP03_07.py").run()
✔️ EP03_07.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_07.cases

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

3.12.8 EP03_08 🧭 Gradiente de Sobel: Gx e Gy

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

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

3.12.8.1 📋 Diretrizes de Implementação

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

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

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

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

3.12.8.2 📌 Restrições Computacionais

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

3.12.8.3 🧠 Fundamentação Teórica

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

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

Entrada:

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

Saída:

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

3.12.8.5 📌 Exemplos

Entrada Saída Observação
3
3
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
Imagem nula: gradiente zero
3
3
0 0 255
0 0 255
0 0 255
0 0 0
0 255 0
0 0 0
Borda vertical central: Gx alto

Simulador do gradiente de Sobel: imagem original, Gx, Gy e magnitude do gradiente.

Kernels de Sobel:
−1
0
+1
−2
0
+2
−1
0
+1
Gx
−1
−2
−1
0
0
0
+1
+2
+1
Gy

Imagem original f

5 × 5 pixels

|∇f| magnitude

√(Gx² + Gy²)

Gx — gradiente horizontal

azul escuro = neg · branco = zero · azul vivo = pos

Gy — gradiente vertical

âmbar escuro = neg · branco = zero · âmbar vivo = pos

Legenda
Vizinhança 3×3 inspecionada
Pixel central
Borda — forçada para 0
Passe o mouse sobre um pixel interno da magnitude para ver a decomposição Gx e Gy.
Figura 3.33: Simulador: Gradiente de Sobel: Gx e Gy
%%writefile EP03_08.py
# Código Python
Writing EP03_08.py
TestSuite("EP03_08.py").run()
✔️ EP03_08.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_08.cases

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

3.12.9 EP03_09 📡 Filtro da Mediana 3×3

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

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

3.12.9.1 📋 Diretrizes de Implementação

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

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

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

3.12.9.2 📌 Restrições Computacionais

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

3.12.9.3 🧠 Fundamentação Teórica

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

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

Entrada:

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

Saída:

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

3.12.9.5 📌 Exemplos

Entrada Saída Observação
3
3
100 100 100
100 0 100
100 100 100
100 100 100
100 100 100
100 100 100
Ponto preto eliminado: mediana de 8×100+1×0 = 100
3
3
50 50 50
50 255 50
50 50 50
50 50 50
50 50 50
50 50 50
Ponto branco (sal) eliminado
Ruído sal (255) e pimenta (0) — ~30% dos pixels internos afetados

Imagem f — com ruído

sal (255) e pimenta (0) visíveis

Resultado g — sem ruído

passe o mouse para inspecionar

Vetor de vizinhança 3×3 — ordenado

passe o mouse num pixel interno do resultado para ver

Legenda
Janela 3×3 capturada
Pixel central inspecionado
Borda — copiada sem filtro
Mediana (posição central)
Ruído (0 ou 255) — eliminado
Passe o mouse sobre um pixel interno do resultado para ver o processo de ordenação.
Figura 3.34: Simulador: Filtro da Mediana 3×3
%%writefile EP03_09.py
# Código Python
Writing EP03_09.py
TestSuite("EP03_09.py").run()
✔️ EP03_09.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_09.cases

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

3.12.10 EP03_10 ✨ Unsharp Masking (USM)

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

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

3.12.10.1 📋 Diretrizes de Implementação

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

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

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

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

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

3.12.10.2 📌 Restrições Computacionais

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

3.12.10.3 🧠 Fundamentação Teórica

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

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

Entrada:

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

Saída:

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

3.12.10.5 📌 Exemplos

Entrada Saída Observação
3
3
0.0
100 100 100
100 100 100
100 100 100
100 100 100
100 100 100
100 100 100
k=0: sem realce
3
3
1.0
50 50 50
50 200 50
50 50 50
50 50 50
50 255 50
50 50 50
k=1: pixel central realçado e saturado

Simulador de Unsharp Masking (USM): visualização do pipeline com imagem original, versão desfocada, máscara de alta frequência e resultado realçado.

Fator de ganho k: k = 1.0 0 = sem realce · 1 = USM padrão · >1 = hiperrealce

① Imagem original f

5 × 5 pixels

④ Resultado g

g = f + 1.0·m

② Desfocado f̄

média 3 × 3

③ Máscara m

m = f − f̄ (altas freq.)

Legenda
Vizinhança 3×3
Pixel inspecionado
Borda — copiada
Máscara positiva
Máscara negativa
Passe o mouse sobre um pixel interno do resultado para rastrear o pipeline completo.
Figura 3.35: Simulador: Unsharp Masking (USM)
%%writefile EP03_10.py
# Código Python
Writing EP03_10.py
TestSuite("EP03_10.py").run()
✔️ EP03_10.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP03_10.cases

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