13. Filtrado y suavizado de imágenes

13.1 Introducción

En visión por computadora, muchas imágenes llegan con ruido, pequeñas variaciones de intensidad o detalles finos que pueden dificultar el análisis posterior. Antes de detectar bordes, segmentar regiones o alimentar ciertos algoritmos, suele ser útil aplicar operaciones de filtrado y suavizado.

Estas técnicas permiten modificar la imagen considerando no solo el valor de un píxel aislado, sino también la información de sus vecinos. Por eso representan un paso natural después de estudiar operaciones matriciales y transformaciones geométricas.

En este tema veremos qué significa filtrar una imagen, qué es un kernel, cómo funciona la convolución y cuáles son los métodos de suavizado más usados en OpenCV.

13.2 ¿Qué es filtrar una imagen?

Filtrar una imagen significa transformar cada píxel en función de una regla que toma en cuenta una vecindad local. En lugar de mirar un solo valor, el algoritmo considera un pequeño bloque alrededor del píxel actual y calcula un nuevo resultado.

El objetivo puede ser muy distinto según el filtro:

  • Suavizar ruido.
  • Resaltar bordes.
  • Detectar estructuras.
  • Atenuar detalles pequeños.
  • Extraer ciertas frecuencias visuales.
Filtrar una imagen significa combinar información local de píxeles vecinos para producir una nueva versión más útil para la tarea que sigue.

13.3 ¿Por qué suavizar?

El suavizado es un tipo particular de filtrado cuyo objetivo principal es reducir variaciones abruptas de intensidad. En otras palabras, intenta hacer la imagen más “estable” o menos ruidosa.

Esto puede ser útil cuando:

  • La imagen tiene ruido generado por el sensor.
  • Queremos reducir pequeños detalles irrelevantes.
  • Buscamos preparar la imagen para detección de bordes.
  • Necesitamos una representación menos sensible a fluctuaciones locales.

Sin embargo, suavizar demasiado también puede borrar información importante. Por eso siempre hay un equilibrio entre reducir ruido y conservar detalle.

13.4 El concepto de vecindad

Cuando hablamos de filtrado, la noción de vecindad es central. Para recalcular un píxel, se suele mirar un bloque cuadrado alrededor de él, por ejemplo de tamaño 3x3, 5x5 o 7x7.

Ese bloque determina cuánta influencia tienen los píxeles cercanos sobre el resultado final. Una vecindad pequeña produce un efecto local más moderado. Una vecindad más grande produce un suavizado más fuerte.

13.5 Kernel o máscara de filtrado

La mayoría de los filtros usan una pequeña matriz llamada kernel o máscara. Esa matriz define cómo se combinan los valores de la vecindad.

Por ejemplo, un kernel simple de promedio 3x3 podría ser:

kernel = [
  [1/9, 1/9, 1/9],
  [1/9, 1/9, 1/9],
  [1/9, 1/9, 1/9]
]

Este kernel asigna el mismo peso a todos los vecinos y calcula el promedio. El resultado es una imagen más suave.

13.6 Convolución: la idea general

La operación matemática que está detrás de muchísimos filtros se llama convolución. De forma intuitiva, consiste en deslizar un kernel sobre la imagen y, en cada posición, combinar los valores locales con los pesos del kernel.

No hace falta memorizar aquí toda la formulación matemática. Lo importante es entender la idea:

  • Se toma una vecindad local.
  • Se multiplica por los pesos del kernel.
  • Se suma el resultado.
  • Ese valor reemplaza o genera el nuevo píxel.

Más adelante veremos que esta misma lógica reaparece en las redes convolucionales.

13.7 Filtros pasa-bajos y pasa-altos

Desde el punto de vista conceptual, muchos filtros pueden agruparse en dos grandes familias:

  • Pasa-bajos: suavizan y eliminan variaciones rápidas.
  • Pasa-altos: resaltan cambios bruscos, como bordes.

En este tema nos enfocaremos sobre todo en filtros de suavizado, es decir, del tipo pasa-bajos. La detección de bordes la estudiaremos en el siguiente tema.

13.8 Ruido en imágenes

El ruido es una variación indeseada en la señal visual. Puede aparecer por múltiples razones:

  • Limitaciones del sensor.
  • Condiciones de iluminación pobres.
  • Compresión.
  • Transmisión imperfecta.
  • Alta sensibilidad ISO en cámaras.

Según el tipo de ruido, algunos filtros serán más adecuados que otros. Por eso conviene no pensar el suavizado como una única herramienta, sino como una familia de métodos.

13.9 Suavizado por promedio

El filtro de promedio o blur simple reemplaza cada píxel por el promedio de sus vecinos. Es una forma intuitiva y directa de suavizar.

Con OpenCV:

import cv2

imagen = cv2.imread("foto1.jpg")

suavizada = cv2.blur(imagen, (5, 5))

cv2.imshow("Imagen original", imagen)
cv2.imshow("Imagen suavizada", suavizada)
cv2.waitKey(0)
cv2.destroyAllWindows()

Aquí el kernel tiene tamaño 5x5. Cuanto mayor sea el kernel, mayor será el efecto de suavizado.

Su ventaja es la simplicidad. Su desventaja es que puede borrar bordes y detalles importantes con bastante facilidad.

13.10 boxFilter

OpenCV también ofrece una función más general para este tipo de suavizado:

suavizada = cv2.boxFilter(imagen, -1, (5, 5))

Conceptualmente es muy similar al blur promedio, pero da más control sobre ciertos parámetros internos.

13.11 Desenfoque gaussiano

El desenfoque gaussiano es uno de los filtros más usados en visión por computadora. A diferencia del promedio simple, no da el mismo peso a todos los vecinos. Los píxeles más cercanos al centro reciben mayor importancia.

Con OpenCV:

gauss = cv2.GaussianBlur(imagen, (5, 5), 0)

Este filtro suele producir resultados más naturales y menos abruptos que el blur promedio. También es muy común como preprocesamiento antes de detección de bordes o umbralización.

13.12 ¿Por qué GaussianBlur es tan usado?

El filtro gaussiano es muy popular por varias razones:

  • Reduce ruido de forma suave.
  • Preserva mejor la estructura global que un promedio uniforme.
  • Está bien fundamentado matemáticamente.
  • Funciona muy bien como paso previo a otros algoritmos.

Por eso aparece una y otra vez en pipelines clásicos de visión.

13.13 Median blur

El filtro de mediana funciona de una manera distinta: en lugar de promediar, toma la mediana de los valores dentro de la vecindad. Esto lo vuelve especialmente útil contra ciertos tipos de ruido impulsivo, como el ruido sal y pimienta.

mediana = cv2.medianBlur(imagen, 5)

Su fortaleza es que puede eliminar puntos anómalos aislados sin difuminar tanto los bordes como un promedio simple.

13.14 Cuándo conviene medianBlur

Este filtro suele ser una buena opción cuando:

  • Hay ruido impulsivo.
  • Queremos conservar bordes razonablemente bien.
  • Los outliers visuales afectan el resultado de un análisis posterior.

No siempre será la mejor elección, pero es un recurso muy valioso cuando el ruido aparece como pequeños puntos erráticos.

13.15 Filtro bilateral

El filtro bilateral es especialmente interesante porque busca suavizar la imagen sin perder tanto los bordes. Para ello tiene en cuenta no solo la distancia espacial entre píxeles, sino también la diferencia de intensidad.

bilateral = cv2.bilateralFilter(imagen, 9, 75, 75)

Este filtro puede preservar bordes mejor que un blur gaussiano, aunque suele ser más costoso computacionalmente.

13.16 Ventajas y costo del filtro bilateral

El bilateral es útil cuando queremos reducir ruido pero mantener discontinuidades importantes, como contornos de objetos. Sin embargo:

  • Es más lento que otros filtros básicos.
  • Puede requerir más ajuste de parámetros.
  • No siempre es necesario si el problema es simple.

Se usa cuando preservar bordes tiene más valor que la máxima velocidad.

13.17 filter2D y kernels personalizados

OpenCV permite aplicar kernels definidos manualmente mediante cv2.filter2D. Esto es muy útil cuando queremos experimentar con filtros personalizados.

import numpy as np

kernel = np.ones((3, 3), np.float32) / 9
resultado = cv2.filter2D(imagen, -1, kernel)

Esta función es importante porque expone de manera muy clara la lógica general del filtrado por convolución.

13.18 Tamaño del kernel

El tamaño del kernel influye mucho en el resultado. Un kernel pequeño, como 3x3, produce un efecto moderado. Un kernel mayor, como 9x9 o 15x15, suaviza mucho más.

Sin embargo, a medida que el kernel crece:

  • Se reduce más el ruido.
  • Se pierden más detalles finos.
  • Los bordes se vuelven menos definidos.
  • El costo computacional puede aumentar.

Elegir bien el tamaño del kernel es una cuestión práctica muy importante.

13.19 Bordes y suavizado

Uno de los dilemas clásicos del filtrado es que muchas técnicas que eliminan ruido también atenúan bordes. Esto ocurre porque los bordes son, precisamente, cambios bruscos de intensidad, y los filtros de suavizado intentan reducir ese tipo de variación.

Por eso, al diseñar un pipeline hay que pensar con cuidado:

  • ¿Cuánto ruido necesitamos reducir?
  • ¿Cuánto detalle podemos perder?
  • ¿Es importante preservar contornos?

La respuesta determinará el tipo de filtro más apropiado.

13.20 Filtrado en imágenes color y en grises

Los filtros pueden aplicarse tanto a imágenes en grises como a imágenes color. En imágenes color, normalmente se procesan los canales de forma coordinada o se opera directamente sobre la estructura multicanal.

En algunos problemas conviene convertir primero a grises. En otros, interesa conservar el color y filtrar la imagen completa. La decisión depende del tipo de información que será relevante después.

13.21 Relación con frecuencia espacial

Desde un punto de vista más conceptual, el suavizado atenúa componentes de alta frecuencia espacial. Esto significa que reduce detalles finos y variaciones rápidas en la imagen.

Aunque no necesitamos profundizar aún en análisis en frecuencia, vale la pena recordar esta intuición:

  • Detalles y ruido suelen estar asociados a variaciones rápidas.
  • El suavizado reduce justamente esas variaciones.

Esta idea reaparecerá más adelante cuando hablemos de detección de bordes y análisis de intensidades.

13.22 Filtrado como preprocesamiento

En muchos pipelines de visión, el filtrado no es el objetivo final, sino un paso previo. Por ejemplo:

  • Antes de detectar bordes, conviene reducir ruido.
  • Antes de umbralizar, a veces conviene estabilizar intensidades locales.
  • Antes de encontrar contornos, puede ser útil eliminar irregularidades pequeñas.

Esto convierte al suavizado en una herramienta de preparación más que en un fin en sí mismo.

13.23 Ejemplo integrado

Veamos ahora un ejemplo integrado más completo, con una aplicación que muestra en tiempo real distintos filtros de suavizado aplicados sobre la cámara:

Aplicación de filtrado y suavizado en tiempo real con OpenCV
import tkinter as tk
from tkinter import ttk, messagebox
import cv2
import numpy as np
from PIL import Image, ImageTk


class AplicacionFiltradoCamara:
    def __init__(self, root):
        self.root = root
        self.root.title("Cámara en vivo - Filtrado y suavizado")
        self.root.geometry("1600x950")

        self.cap = None
        self.ejecutando = False

        self.ancho_celda = 300
        self.alto_celda = 180

        self.crear_interfaz()

    # ---------------------------------------------------------
    # INTERFAZ
    # ---------------------------------------------------------
    def crear_interfaz(self):
        contenedor = ttk.Frame(self.root, padding=10)
        contenedor.pack(fill="both", expand=True)

        # Panel izquierdo
        panel_izquierdo = ttk.Frame(contenedor)
        panel_izquierdo.pack(side="left", fill="y", padx=(0, 10))

        ttk.Label(
            panel_izquierdo,
            text="Cámara y parámetros",
            font=("Arial", 12, "bold")
        ).pack(anchor="w", pady=(0, 8))

        ttk.Button(panel_izquierdo, text="Iniciar cámara", command=self.iniciar_camara).pack(fill="x", pady=2)
        ttk.Button(panel_izquierdo, text="Detener cámara", command=self.detener_camara).pack(fill="x", pady=2)

        ttk.Separator(panel_izquierdo, orient="horizontal").pack(fill="x", pady=10)

        ttk.Label(panel_izquierdo, text="Tamaño kernel general").pack(anchor="w")
        self.scale_kernel = tk.Scale(panel_izquierdo, from_=1, to=25, orient="horizontal", length=250)
        self.scale_kernel.set(5)
        self.scale_kernel.pack()

        ttk.Label(panel_izquierdo, text="Tamaño kernel mediana").pack(anchor="w")
        self.scale_kernel_mediana = tk.Scale(panel_izquierdo, from_=1, to=25, orient="horizontal", length=250)
        self.scale_kernel_mediana.set(5)
        self.scale_kernel_mediana.pack()

        ttk.Label(panel_izquierdo, text="Sigma Color bilateral").pack(anchor="w")
        self.scale_sigma_color = tk.Scale(panel_izquierdo, from_=1, to=150, orient="horizontal", length=250)
        self.scale_sigma_color.set(75)
        self.scale_sigma_color.pack()

        ttk.Label(panel_izquierdo, text="Sigma Space bilateral").pack(anchor="w")
        self.scale_sigma_space = tk.Scale(panel_izquierdo, from_=1, to=150, orient="horizontal", length=250)
        self.scale_sigma_space.set(75)
        self.scale_sigma_space.pack()

        ttk.Label(panel_izquierdo, text="Diámetro bilateral").pack(anchor="w")
        self.scale_d_bilateral = tk.Scale(panel_izquierdo, from_=1, to=25, orient="horizontal", length=250)
        self.scale_d_bilateral.set(9)
        self.scale_d_bilateral.pack()

        ttk.Label(panel_izquierdo, text="Interpolación visual").pack(anchor="w")
        self.combo_interpolacion = ttk.Combobox(
            panel_izquierdo,
            state="readonly",
            values=["INTER_NEAREST", "INTER_LINEAR", "INTER_CUBIC", "INTER_AREA"],
            width=20
        )
        self.combo_interpolacion.current(1)
        self.combo_interpolacion.pack(fill="x", pady=4)

        self.var_espejo = tk.BooleanVar(value=True)
        ttk.Checkbutton(
            panel_izquierdo,
            text="Mostrar cámara espejada",
            variable=self.var_espejo
        ).pack(anchor="w", pady=6)

        ttk.Separator(panel_izquierdo, orient="horizontal").pack(fill="x", pady=10)

        texto_info = (
            "Grilla en tiempo real:\n"
            "1. Original\n"
            "2. Blur promedio\n"
            "3. BoxFilter\n"
            "4. Gaussian Blur\n"
            "5. Median Blur\n"
            "6. Bilateral Filter\n"
            "7. filter2D personalizado\n"
            "8. Gris + Gaussian\n"
            "9. Suavizado fuerte"
        )
        ttk.Label(panel_izquierdo, text=texto_info, foreground="blue", justify="left").pack(anchor="w")

        self.label_estado = ttk.Label(panel_izquierdo, text="Cámara detenida", foreground="red")
        self.label_estado.pack(anchor="w", pady=(12, 0))

        # Panel derecho
        panel_derecho = ttk.Frame(contenedor)
        panel_derecho.pack(side="left", fill="both", expand=True)

        ttk.Label(
            panel_derecho,
            text="Filtrado y suavizado en tiempo real",
            font=("Arial", 13, "bold")
        ).pack(anchor="center", pady=(0, 10))

        self.frame_grilla = ttk.Frame(panel_derecho)
        self.frame_grilla.pack(fill="both", expand=True)

        self.labels_imagen = {}
        self._crear_grilla_imagenes()

        self.root.protocol("WM_DELETE_WINDOW", self.al_cerrar)

    def _crear_grilla_imagenes(self):
        nombres = [
            "Original", "Blur promedio", "BoxFilter",
            "Gaussian Blur", "Median Blur", "Bilateral Filter",
            "filter2D personalizado", "Gris + Gaussian", "Suavizado fuerte"
        ]

        indice = 0
        for fila in range(3):
            self.frame_grilla.rowconfigure(fila, weight=1)
            for col in range(3):
                self.frame_grilla.columnconfigure(col, weight=1)

                marco = ttk.Frame(self.frame_grilla, relief="solid", borderwidth=1, padding=5)
                marco.grid(row=fila, column=col, padx=5, pady=5, sticky="nsew")

                titulo = ttk.Label(marco, text=nombres[indice], font=("Arial", 10, "bold"))
                titulo.pack(anchor="center", pady=(0, 4))

                label = ttk.Label(marco, anchor="center")
                label.pack(fill="both", expand=True)

                self.labels_imagen[nombres[indice]] = label
                indice += 1

    # ---------------------------------------------------------
    # CÁMARA
    # ---------------------------------------------------------
    def iniciar_camara(self):
        if self.ejecutando:
            return

        self.cap = cv2.VideoCapture(0)
        if not self.cap.isOpened():
            messagebox.showerror("Error", "No se pudo abrir la cámara.")
            return

        self.ejecutando = True
        self.label_estado.config(text="Cámara en ejecución", foreground="green")
        self.actualizar_video()

    def detener_camara(self):
        self.ejecutando = False
        if self.cap is not None:
            self.cap.release()
            self.cap = None
        self.label_estado.config(text="Cámara detenida", foreground="red")

    def al_cerrar(self):
        self.detener_camara()
        self.root.destroy()

    # ---------------------------------------------------------
    # UTILIDADES
    # ---------------------------------------------------------
    def obtener_interpolacion(self):
        nombre = self.combo_interpolacion.get()
        mapa = {
            "INTER_NEAREST": cv2.INTER_NEAREST,
            "INTER_LINEAR": cv2.INTER_LINEAR,
            "INTER_CUBIC": cv2.INTER_CUBIC,
            "INTER_AREA": cv2.INTER_AREA
        }
        return mapa.get(nombre, cv2.INTER_LINEAR)

    def kernel_impar(self, valor):
        k = int(valor)
        if k < 1:
            k = 1
        if k % 2 == 0:
            k += 1
        return k

    def mostrar_en_label(self, frame_bgr, label):
        if frame_bgr is None:
            return

        if len(frame_bgr.shape) == 2:
            frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_GRAY2RGB)
        else:
            frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)

        h, w = frame_rgb.shape[:2]
        escala = min(self.ancho_celda / w, self.alto_celda / h)

        nuevo_w = max(1, int(w * escala))
        nuevo_h = max(1, int(h * escala))

        frame_redim = cv2.resize(frame_rgb, (nuevo_w, nuevo_h), interpolation=self.obtener_interpolacion())

        lienzo = np.zeros((self.alto_celda, self.ancho_celda, 3), dtype=np.uint8)

        x = (self.ancho_celda - nuevo_w) // 2
        y = (self.alto_celda - nuevo_h) // 2
        lienzo[y:y + nuevo_h, x:x + nuevo_w] = frame_redim

        imagen_pil = Image.fromarray(lienzo)
        imagen_tk = ImageTk.PhotoImage(imagen_pil)

        label.configure(image=imagen_tk)
        label.image = imagen_tk

    # ---------------------------------------------------------
    # FILTROS
    # ---------------------------------------------------------
    def filtro_blur_promedio(self, frame):
        k = self.kernel_impar(self.scale_kernel.get())
        return cv2.blur(frame, (k, k))

    def filtro_boxfilter(self, frame):
        k = self.kernel_impar(self.scale_kernel.get())
        return cv2.boxFilter(frame, -1, (k, k))

    def filtro_gaussian(self, frame):
        k = self.kernel_impar(self.scale_kernel.get())
        return cv2.GaussianBlur(frame, (k, k), 0)

    def filtro_mediana(self, frame):
        k = self.kernel_impar(self.scale_kernel_mediana.get())
        return cv2.medianBlur(frame, k)

    def filtro_bilateral(self, frame):
        d = self.kernel_impar(self.scale_d_bilateral.get())
        sigma_color = self.scale_sigma_color.get()
        sigma_space = self.scale_sigma_space.get()
        return cv2.bilateralFilter(frame, d, sigma_color, sigma_space)

    def filtro_personalizado(self, frame):
        kernel = np.array([
            [1, 2, 1],
            [2, 4, 2],
            [1, 2, 1]
        ], dtype=np.float32)
        kernel = kernel / kernel.sum()
        return cv2.filter2D(frame, -1, kernel)

    def filtro_gris_gaussian(self, frame):
        gris = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        k = self.kernel_impar(self.scale_kernel.get())
        return cv2.GaussianBlur(gris, (k, k), 0)

    def filtro_suavizado_fuerte(self, frame):
        k = self.kernel_impar(self.scale_kernel.get())
        k_fuerte = min(k + 8, 31)
        if k_fuerte % 2 == 0:
            k_fuerte += 1
        return cv2.GaussianBlur(frame, (k_fuerte, k_fuerte), 0)

    # ---------------------------------------------------------
    # ACTUALIZACIÓN
    # ---------------------------------------------------------
    def actualizar_video(self):
        if not self.ejecutando or self.cap is None:
            return

        ok, frame = self.cap.read()
        if not ok:
            self.root.after(30, self.actualizar_video)
            return

        frame = cv2.resize(frame, (640, 480))

        if self.var_espejo.get():
            frame_base = cv2.flip(frame, 1)
        else:
            frame_base = frame.copy()

        original = frame_base
        blur_promedio = self.filtro_blur_promedio(frame_base)
        boxfilter = self.filtro_boxfilter(frame_base)
        gaussian = self.filtro_gaussian(frame_base)
        mediana = self.filtro_mediana(frame_base)
        bilateral = self.filtro_bilateral(frame_base)
        personalizado = self.filtro_personalizado(frame_base)
        gris_gaussian = self.filtro_gris_gaussian(frame_base)
        fuerte = self.filtro_suavizado_fuerte(frame_base)

        self.mostrar_en_label(original, self.labels_imagen["Original"])
        self.mostrar_en_label(blur_promedio, self.labels_imagen["Blur promedio"])
        self.mostrar_en_label(boxfilter, self.labels_imagen["BoxFilter"])
        self.mostrar_en_label(gaussian, self.labels_imagen["Gaussian Blur"])
        self.mostrar_en_label(mediana, self.labels_imagen["Median Blur"])
        self.mostrar_en_label(bilateral, self.labels_imagen["Bilateral Filter"])
        self.mostrar_en_label(personalizado, self.labels_imagen["filter2D personalizado"])
        self.mostrar_en_label(gris_gaussian, self.labels_imagen["Gris + Gaussian"])
        self.mostrar_en_label(fuerte, self.labels_imagen["Suavizado fuerte"])

        self.root.after(30, self.actualizar_video)


if __name__ == "__main__":
    root = tk.Tk()
    app = AplicacionFiltradoCamara(root)
    root.mainloop()

Este programa permite comparar visualmente varios métodos de suavizado sobre video en vivo, lo que ayuda a desarrollar intuición práctica sobre sus diferencias y aplicaciones.

13.24 ¿Cómo elegir un filtro?

No existe un filtro universalmente mejor. La elección depende del problema:

  • Si queremos algo simple y rápido, el promedio puede ser suficiente.
  • Si buscamos suavizado general robusto, GaussianBlur suele ser una gran opción.
  • Si el ruido es impulsivo, medianBlur puede funcionar mejor.
  • Si necesitamos preservar bordes, el bilateral puede ser preferible.

En proyectos reales, esta decisión muchas veces se toma experimentando con ejemplos representativos del problema.

13.25 Errores comunes

Al empezar a trabajar con filtrado y suavizado, algunos errores frecuentes son:

  • Aplicar un kernel demasiado grande y destruir detalle útil.
  • Elegir un filtro inadecuado para el tipo de ruido.
  • No comparar visualmente el antes y el después.
  • Asumir que más suavizado siempre es mejor.
  • Olvidar que ciertos filtros son más costosos que otros.

La clave está en filtrar lo suficiente para ayudar al siguiente paso, pero no tanto como para perjudicar la información relevante.

13.26 Qué debes recordar de este tema

  • Filtrar una imagen significa recalcular píxeles usando información de su vecindad.
  • El suavizado reduce ruido y variaciones bruscas, pero puede eliminar detalle.
  • Los conceptos de kernel y convolución son centrales para entender el filtrado.
  • OpenCV ofrece filtros importantes como blur, GaussianBlur, medianBlur y bilateralFilter.
  • Cada filtro tiene ventajas y limitaciones según el tipo de ruido y el objetivo del problema.
  • El filtrado suele funcionar como etapa de preprocesamiento para análisis posterior.

13.27 Conclusión

El filtrado y el suavizado de imágenes son herramientas fundamentales porque permiten controlar ruido, estabilizar la señal visual y preparar la imagen para tareas posteriores más sensibles. Aunque conceptualmente simples, tienen un impacto enorme en la calidad del pipeline.

Dominar estos filtros también ayuda a entender mejor cómo la información local de una imagen puede combinarse y transformarse para lograr un objetivo concreto.

En el próximo tema estudiaremos la detección de bordes, donde veremos cómo pasar del suavizado a técnicas que justamente buscan resaltar cambios bruscos de intensidad.