Filtragem Espacial#

Visão Computacional | Prof. Dr. Denis Mayr Lima Martins

Entendendo Convolução#

Convolução é um operador linear que, a partir de duas funções dadas, resulta numa terceira que mede a soma do produto dessas funções ao longo da região subentendida pela superposição delas em função do deslocamento existente entre elas.” - Wikipedia.

Caso 1D: https://antoinebrl.github.io/blog/conv1d/#

equação convolução 1D

animação convolução 1D

Caso 2D: https://objetos.github.io/docs/demos/image_conv_vis/#

Kernel: matriz de pesos \((w_{mn})\).

\(g(x,y) = w(x,y)*f(x,y) = \sum_{s=-a}^{a}\sum_{t=-b}^{b} w(s,t)f(x-s,y-t)\)

  1. Coloque o centro do kernel sobre o pixel que você está analisando.

  2. Multiplique cada elemento do kernel pelo valor do pixel correspondente na imagem (a região coberta).

  3. Some todos esses produtos para obter um único número, que se torna o novo valor desse pixel na saída.

Assim, a convolução é simplesmente “deslizar um pequeno molde de números sobre a imagem e calcular uma soma ponderada” em cada posição, produzindo efeitos variados dependendo do padrão escolhido para o kernel.

Propriedades da Convolução#

Comutatividade

Associatividade

Distributividade

Associatividade com multiplicação escalar

Imports e Preparação#

import cv2
import numpy as np
import matplotlib.pyplot as plt
from urllib.request import urlopen

SEED = 1234
np.random.seed(SEED)
A module that was compiled using NumPy 1.x cannot be run in
NumPy 2.2.5 as it may crash. To support both 1.x and 2.x
versions of NumPy, modules must be compiled with NumPy 2.0.
Some module may need to rebuild instead e.g. with 'pybind11>=2.12'.

If you are a user of the module, the easiest solution will be to
downgrade to 'numpy<2' or try to upgrade the affected module.
We expect that some modules will need time to support NumPy 2.

Traceback (most recent call last):  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel_launcher.py", line 17, in <module>
    app.launch_new_instance()
  File "/opt/anaconda3/lib/python3.11/site-packages/traitlets/config/application.py", line 992, in launch_instance
    app.start()
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelapp.py", line 701, in start
    self.io_loop.start()
  File "/opt/anaconda3/lib/python3.11/site-packages/tornado/platform/asyncio.py", line 195, in start
    self.asyncio_loop.run_forever()
  File "/opt/anaconda3/lib/python3.11/asyncio/base_events.py", line 607, in run_forever
    self._run_once()
  File "/opt/anaconda3/lib/python3.11/asyncio/base_events.py", line 1922, in _run_once
    handle._run()
  File "/opt/anaconda3/lib/python3.11/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 534, in dispatch_queue
    await self.process_one()
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 523, in process_one
    await dispatch(*args)
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 429, in dispatch_shell
    await result
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/kernelbase.py", line 767, in execute_request
    reply_content = await reply_content
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/ipkernel.py", line 429, in do_execute
    res = shell.run_cell(
  File "/opt/anaconda3/lib/python3.11/site-packages/ipykernel/zmqshell.py", line 549, in run_cell
    return super().run_cell(*args, **kwargs)
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3051, in run_cell
    result = self._run_cell(
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3106, in _run_cell
    result = runner(coro)
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/async_helpers.py", line 129, in _pseudo_sync_runner
    coro.send(None)
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3311, in run_cell_async
    has_raised = await self.run_ast_nodes(code_ast.body, cell_name,
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3493, in run_ast_nodes
    if await self.run_code(code, result, async_=asy):
  File "/opt/anaconda3/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3553, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "/var/folders/p7/p37cm2fj10xgjrjj5rzdm66c0000gn/T/ipykernel_59184/488522286.py", line 3, in <module>
    import matplotlib.pyplot as plt
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/__init__.py", line 161, in <module>
    from . import _api, _version, cbook, _docstring, rcsetup
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/rcsetup.py", line 27, in <module>
    from matplotlib.colors import Colormap, is_color_like
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/colors.py", line 57, in <module>
    from matplotlib import _api, _cm, cbook, scale
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/scale.py", line 22, in <module>
    from matplotlib.ticker import (
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/ticker.py", line 143, in <module>
    from matplotlib import transforms as mtransforms
  File "/opt/anaconda3/lib/python3.11/site-packages/matplotlib/transforms.py", line 49, in <module>
    from matplotlib._path import (
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
AttributeError: _ARRAY_API not found
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[1], line 3
      1 import cv2
      2 import numpy as np
----> 3 import matplotlib.pyplot as plt
      4 from urllib.request import urlopen
      6 SEED = 1234

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/__init__.py:161
    157 from packaging.version import parse as parse_version
    159 # cbook must import matplotlib only within function
    160 # definitions, so it is safe to import from it here.
--> 161 from . import _api, _version, cbook, _docstring, rcsetup
    162 from matplotlib.cbook import sanitize_sequence
    163 from matplotlib._api import MatplotlibDeprecationWarning

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/rcsetup.py:27
     25 from matplotlib import _api, cbook
     26 from matplotlib.cbook import ls_mapper
---> 27 from matplotlib.colors import Colormap, is_color_like
     28 from matplotlib._fontconfig_pattern import parse_fontconfig_pattern
     29 from matplotlib._enums import JoinStyle, CapStyle

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/colors.py:57
     55 import matplotlib as mpl
     56 import numpy as np
---> 57 from matplotlib import _api, _cm, cbook, scale
     58 from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS
     61 class _ColorMapping(dict):

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/scale.py:22
     20 import matplotlib as mpl
     21 from matplotlib import _api, _docstring
---> 22 from matplotlib.ticker import (
     23     NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter,
     24     NullLocator, LogLocator, AutoLocator, AutoMinorLocator,
     25     SymmetricalLogLocator, AsinhLocator, LogitLocator)
     26 from matplotlib.transforms import Transform, IdentityTransform
     29 class ScaleBase:

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/ticker.py:143
    141 import matplotlib as mpl
    142 from matplotlib import _api, cbook
--> 143 from matplotlib import transforms as mtransforms
    145 _log = logging.getLogger(__name__)
    147 __all__ = ('TickHelper', 'Formatter', 'FixedFormatter',
    148            'NullFormatter', 'FuncFormatter', 'FormatStrFormatter',
    149            'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter',
   (...)
    155            'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator',
    156            'SymmetricalLogLocator', 'AsinhLocator', 'LogitLocator')

File /opt/anaconda3/lib/python3.11/site-packages/matplotlib/transforms.py:49
     46 from numpy.linalg import inv
     48 from matplotlib import _api
---> 49 from matplotlib._path import (
     50     affine_transform, count_bboxes_overlapping_bbox, update_path_extents)
     51 from .path import Path
     53 DEBUG = False

ImportError: numpy.core.multiarray failed to import
def show_images_playground(original, convolved, img_size):
    # Mostra as imagens original o resultado da convolução
    figure, axarr = plt.subplots(1,2, figsize=(6,6))
    axarr[0].imshow(original, cmap= 'gray')
    axarr[1].imshow(convolved, cmap= 'gray')
    for ax in axarr.ravel():
        # Configura major ticks
        ax.set_xticks(np.arange(0, img_size, 1))
        ax.set_yticks(np.arange(0, img_size, 1))
        # Configura minor ticks
        ax.set_xticks(np.arange(-.5, img_size, 1), minor=True)
        ax.set_yticks(np.arange(-.5, img_size, 1), minor=True)
        # Configura gridlines baseadas nos minor ticks
        ax.grid(which='minor', color='b', linestyle='-', linewidth=2)
        # Remove minor ticks
        ax.tick_params(which='minor', bottom=False, left=False)
    plt.show()

def show_images(original, convolved):
    figure, axarr = plt.subplots(1,2, figsize=(10,10))
    axarr[0].imshow(original, cmap= 'gray')
    axarr[1].imshow(convolved, cmap= 'gray')
    
    # Remove plt grid e ticks
    for ax in axarr.ravel():
        ax.set_axis_off()
    
    plt.show()

Playground: Testando filtros imagem binária#

# Cria imagem aletória
img_size = 16
rnd_img = np.random.randint(0, 2, size=(img_size,img_size),dtype=np.uint8)
kernel = np.array( # Define um kernel
    [[0, 0, 0],
     [0, 0, 1],
     [0, 0, 0]],
    np.float32)
resultado_img = cv2.filter2D(rnd_img, -1, kernel) # Convolução
show_images_playground(rnd_img, resultado_img, img_size)
../_images/bba22054aa5226706c4ec6c8142c9df23240f70bfecc29e348edb197023abd95.png

Carregamento de Imagem#

def url_to_image(url, readFlag=cv2.IMREAD_GRAYSCALE):
    # download the image, convert it to a NumPy array, and then read
    # it into OpenCV format
    resp = urlopen(url)
    image = np.asarray(bytearray(resp.read()), dtype="uint8")
    image = cv2.imdecode(image, readFlag)
    # return the image
    return image
img = url_to_image("https://www.flickr.com/photo_download.gne?id=16504233985&secret=9f1060624e&size=q&source=photoPageEngagement")
plt.figure(figsize=(3,3))
plt.imshow(img, cmap="gray")
plt.axis("off")
plt.show()
../_images/586380c22a2cd6447d709f951ec0bebe9b1b2124a9b00302c3802b3716ba101c.png

Filtro de Média#

Utilizado para suavização da imagem.

Diminui a nitidez pela redução de fortes transições de intensidade entre os pixels.

Filtro de Média: Aplicação#

# Define um kernel de suavização (média)
kernel = np.ones((3,3), np.float32) / 9

# Aplica a convolução com o kernel
media_img = cv2.filter2D(img, -1, kernel)

# Mostra as imagens original e suavizada
show_images(img, media_img)
../_images/585a67914c7c3ac7a854649c032d725c6fc9b87e47d30486ef86b6247bb243bb.png

Filtro de Média: Kernels de diferentes tamanhos#

# Cria a estrutura de plots para as imagens
figure, axarr = plt.subplots(1,5, figsize=(10,10))
axarr[0].imshow(img, cmap= 'gray')
axarr[0].set_title("Imagem Original")
for cnt, size in enumerate([3, 5, 9, 15]):
  # Define um kernel de suavização (média)
  kernel = np.ones((size,size), np.float32) / (size**2)
  # Aplica a convolução com o kernel
  imagem_suavizada = cv2.filter2D(img, -1, kernel)
  # Mostra as imagens original e suavizada
  axarr[cnt+1].imshow(imagem_suavizada, cmap= 'gray')
  axarr[cnt+1].set_title(f"Kernel ({size} x {size})")
# Remove plt grid e ticks
for ax in axarr.ravel():
    ax.set_axis_off()
plt.tight_layout()
plt.show()
../_images/a2b705fcfd8ccd25232529acb227e9ae11995c1ed3ede8017cfeb3677f53b83b.png

Filtro de Média: Diferentes valores de Kernel#

kernel_media = np.array([
    [1, 2, 1],
    [2, 4, 2],
    [1, 2, 1],
]).astype(np.float16)
kernel_media /= kernel_media.sum()
imagem_suavizada = cv2.filter2D(img, -1, kernel_media)
figure, axarr = plt.subplots(1,2, figsize=(6,6))
axarr[0].imshow(img, cmap= 'gray')
axarr[1].imshow(imagem_suavizada, cmap= 'gray')
for ax in axarr.ravel():
    ax.set_axis_off()
plt.show()
../_images/669df52762fa18d635e8e11c6effceb26f87623505f6c36d614314f7647205ce.png

Filtro Gaussiano#

Suavização: Filtro de média ponderada, mas com seus pesos seguindo uma aproximação da distribuição gaussiana.

\(G(x,y;\sigma)=\frac{1}{2\pi\sigma^{2}}\exp\!\left(-\,\frac{x^{2}+y^{2}}{2\sigma^{2}}\right)\)

Note que o valor resultante para o elemento central pode ser um valor não existente na imagem original.

Fonte da Imagem: Towards AI.

Filtro Gaussiano: Aplicação#

# Suavização Gaussiana (Filtro Gaussiano)
kernel_size = (5, 5)
sigma = 1.0
# Aplica o filtro Gaussiano
blurred = cv2.GaussianBlur(img, kernel_size, sigmaX=sigma)
figure, axarr = plt.subplots(1,2, figsize=(6,6))
axarr[0].imshow(img, cmap= 'gray')
axarr[1].imshow(blurred, cmap= 'gray')
# Remove plt grid e ticks
for ax in axarr.ravel():
    ax.set_axis_off()
plt.show()
../_images/8a0b7eddb8cab5c83f263829957cd240d5f8000ce812c8c453e0e796169dca92.png

Filtro Gaussiano: Testando diferentes valores de \(\sigma\)#

# Cria a estrutura de plots para as imagens
figure, axarr = plt.subplots(1,5, figsize=(10,5))
axarr[0].imshow(img, cmap= 'gray')
axarr[0].set_title("Imagem Original")
for cnt, sigma in enumerate([0.5, 1, 1.5, 2]):
  # Aplica o filtro Gaussiano
  gauss_img = cv2.GaussianBlur(img, kernel_size, sigmaX=sigma)
  # Mostra as imagens original e suavizada
  axarr[cnt+1].imshow(gauss_img, cmap= 'gray')
  axarr[cnt+1].set_title(f"Sigma: {sigma}")
  ax.set_axis_off()
# Remove plt grid e ticks
for ax in axarr.ravel():
    ax.set_axis_off()
plt.tight_layout()
plt.show()
../_images/2feabcca080e6e125ab16f11607fb9853770221768dbbce15f06aa6710a44918.png

Comparação: Filtro de Média versus Filtro Gaussiano#

kernel_size = (5, 5)
avg_img = cv2.blur(img, kernel_size)
gaussian_img = cv2.GaussianBlur(img, kernel_size, sigmaX=1.0)
figure, axarr = plt.subplots(1,3, figsize=(8,8))
axarr[0].imshow(img, cmap= 'gray')
axarr[0].set_title("Imagem Original")
axarr[1].imshow(avg_img, cmap= 'gray')
axarr[1].set_title(f"Filtro Média {str(kernel_size).replace(',', ' x ')}")
axarr[2].imshow(gaussian_img, cmap= 'gray')
axarr[2].set_title(f"Filtro de Gaussiano {str(kernel_size).replace(',', ' x ')}")
for ax in axarr.ravel():
    ax.set_axis_off()
plt.show()
../_images/b6e40ea1809f81f73db4535690e429df9d42a75a8348cc42f3c79af97a67a257.png

Filtro Mediana#

  • Elimina picos isolados (outliers).

  • Substitui o valor de um pixel pela mediana dos valores de intensidade na vizinhança desse pixel.

Fonte da Imagem: ResearchGate.

Filtro Mediana: Aplicação#

# Aplica filtro de mediana
median_filtered = cv2.medianBlur(img, ksize=3)
figure, axarr = plt.subplots(1,2, figsize=(8,8))
axarr[0].imshow(img, cmap= 'gray')
axarr[0].set_title("Imagem Original")
axarr[1].imshow(median_filtered, cmap= 'gray')
axarr[1].set_title("Imagem Filtrada pela Mediana")
for ax in axarr.ravel():
    ax.set_axis_off()
plt.show()
../_images/33282367ecd5cb5e461b572ee1222d1ddba3db85acd978c593655f8dcde291c6.png

Filtro de Mediana: Removendo Ruído Salt-and-Pepper#

def add_salt_and_pepper_noise(image, noise_ratio=0.02):
    noisy_image = image.copy()
    h, w = noisy_image.shape
    noisy_pixels = int(h * w * noise_ratio)
    for _ in range(noisy_pixels):
        row, col = np.random.randint(0, h), np.random.randint(0, w)
        if np.random.rand() < 0.5:
            noisy_image[row, col] = 0
        else:
            noisy_image[row, col] = 255
    return noisy_image
    
ruido_img = add_salt_and_pepper_noise(img)
mediana_filtrada_img = cv2.medianBlur(ruido_img, ksize=3)
figure, axarr = plt.subplots(1,3, figsize=(11,8))
axarr[0].imshow(img, cmap= 'gray')
axarr[0].set_title("Imagem Original")
axarr[1].imshow(ruido_img, cmap= 'gray')
axarr[1].set_title("Imagem Ruidosa (Salt and Pepper)")
axarr[2].imshow(mediana_filtrada_img, cmap= 'gray')
axarr[2].set_title("Imagem Filtrada pela Mediana")
# Remove plt grid e ticks
for ax in axarr.ravel():
    ax.set_axis_off()
plt.show()
../_images/f3e1cd3c4a478cb8adbe802e1e9ed3362eedcf2f74c0a45657392aaf9970900f.png

Filtro Sobel#

  • O filtro de Sobel é um operador discreto de derivação, projetado para estimar a magnitude do gradiente de intensidade em imagens digitais. \(\rightarrow\) Combina duas convoluções separadas: uma kernel \(h_x\) que responde à variação horizontal e outra \(h_y\) que captura a variação vertical.

  • A combinação das respostas \(\sqrt{(I * h_x)^2 + (I * h_y)^2}\) fornece uma aproximação robusta da magnitude do gradiente, enquanto o sinal de cada componente indica a direção local do aumento ou diminuição de intensidade.

  • Eficaz na detecção de bordas: Enfatiza regiões com transições rápidas de luminância. Suaviza ruídos graças à integração de ponderações gaussiana-like nos kernels.

Espectro Eletromagnético Filtro Sobel. Fonte: Digital Ocean.

Filtro Sobel: Exemplo#

Espectro Eletromagnético Exemplo de aplicação de Filtro Sobel. Fonte: Luísa Mendes Heise @Turing Talks.

Filtro Sobel: Aplicação#

sobel_img = cv2.Sobel(img, ddepth=-1, dx=1, dy=0, ksize=3)

figure, axarr = plt.subplots(1,2, figsize=(8,8))
axarr[0].imshow(img, cmap= 'gray')
axarr[0].set_title("Imagem Original")
axarr[1].imshow(sobel_img, cmap= 'gray')
axarr[1].set_title("Filtro Sobel")
for ax in axarr.ravel():
    ax.set_axis_off()
plt.show()
../_images/27e9322a34df542f33022397676d830c4465eb1cf3339ba288149e5a97e4579c.png

Filtro Laplaciano#

  • O operador laplaciano é uma segunda derivada discreta que mede a divergência do gradiente de intensidade de uma imagem.

  • \(Laplace(f) = \dfrac{\partial^{2} f}{\partial x^{2}} + \dfrac{\partial^{2} f}{\partial y^{2}}\)

  • Captura a divergência do gradiente de intensidade de uma imagem, ou seja, quantifica as regiões onde a curvatura da luminância muda drasticamente.

  • Soma ponderada dos vizinhos diretos do pixel central, subtraindo quatro vezes seu valor.

  • Destaca pontos de mudança abrupta — bordas e cantos — mas não indica a direção dessas transições.

  • Kernels laplacianos geralmente incluem valores negativos na vizinhança e um valor igual à soma destes valores negativos no centro, mas com sinal invertido: $\(K = \begin{bmatrix}0 & -1 & 0\\ -1 & 4 & -1\\ 0 & -1 & 0\end{bmatrix}\)$

Filtro Laplaciano: Aplicação#

laplacean_img = cv2.Laplacian(img, ddepth=-1, ksize=3)

figure, axarr = plt.subplots(1,2, figsize=(8,8))
axarr[0].imshow(img, cmap= 'gray')
axarr[0].set_title("Imagem Original")
axarr[1].imshow(laplacean_img, cmap= 'gray')
axarr[1].set_title("Filtro Laplaciano")
for ax in axarr.ravel():
    ax.set_axis_off()
plt.show()
../_images/f677c8be41aec3dcdedc435eb81e9c71ee2426c361c900bbdff388b8f7d706a4.png

Outros Filtros#

Prewitt

  • Operador linear de 3×3 usado para detecção de bordas.

  • Calcula aproximações das derivadas parciais ∂I/∂x e ∂I/∂y.
    $\( G_x=\begin{bmatrix}-1&0&1\\-1&0&1\\-1&0&1\end{bmatrix},\quad G_y=\begin{bmatrix}-1&-1&-1\\0&0&0\\1&1&1\end{bmatrix} \)$

  • Resultado: imagem com bordas destacadas; suaviza ruído moderado, porém menos sensível que Sobel.

Outros Filtros (cont.)#

Max (Filtro de Maxima)

  • Operador não linear que substitui cada pixel pelo maior valor dentro do vizinho definido.

  • Reduz ruído impulsivo “salt & pepper”, preservando detalhes mais claros.

  • Resultado: imagem com regiões saturadas ampliadas; pode gerar bordas suavizadas em áreas escuras.

Min (Filtro de Minima)

  • Operador não linear que substitui cada pixel pelo menor valor dentro do vizinho.

  • Útil para eliminar ruído impulsivo salt and pepper nas áreas claras.

  • Resultado: diminuição de brilho local, bordas mais suaves em regiões escuras.

Resumo#


  • Convolução: ferramenta matemática central.

  • Filtros lineares (média, Gaussiano) oferecem suavização controlada; a separabilidade do kernel pode reduzir drasticamente o custo computacional.

  • Filtros não‑lineares (mediana, adaptativos) são essenciais para robustez contra ruído impulsivo e preservação de bordas em cenários adversos.

  • A escolha entre suavização e realce depende da aplicação: imagens médicas, visão robótica ou processamento de vídeo em tempo real.

Leitura Recomendada: Seções 3.4 a 3.7.