Filtrado en el Dominio de la Frecuencia: Fundamentos y Aplicaciones

Francisco ZavalaFrancisco Zavala
21 min read

Las operaciones de filtrado suelen realizarse en el dominio espacial, a través de convoluciones con máscaras (o kernels) que operan directamente sobre los píxeles. Sin embargo, existe una alternativa: el filtrado en el dominio de la frecuencia, basado en la Transformada Discreta de Fourier (DFT).

Este enfoque aprovecha una propiedad fundamental de la teoría de señales: el teorema de convolución circular, que establece que la convolución espacial entre dos señales es equivalente a la multiplicación de sus representaciones en frecuencia. Esto permite transformar una operación local —como un desenfoque— en una operación global pero computacionalmente eficiente. En el caso de imágenes, esto significa que podemos aplicar un filtro multiplicando espectros, para luego recuperar la imagen filtrada mediante la transformada inversa.

Además de la eficiencia, una de las grandes ventajas del dominio frecuencial es la intuitividad en el diseño de filtros. Es más sencillo entender cómo un filtro afecta una imagen al observar su respuesta en frecuencia que al inspeccionar directamente los valores de un kernel en el espacio. Esta perspectiva permite crear filtros como el pasa-bajos ideal, el filtro de Butterworth, o el filtro homomórfico, cada uno con aplicaciones específicas que van desde la restauración de imágenes degradadas hasta el realce de detalles.

1. Fundamentos del Filtrado en el Dominio de la Frecuencia

1.1. Equivalencia entre convolución espacial y multiplicación frecuencial

Uno de los pilares del análisis en frecuencia es el teorema de la convolución, que establece una relación directa entre la convolución en el dominio espacial y la multiplicación en el dominio frecuencial. Formalmente, si se tiene una imagen g(x, y) y un filtro espacial h(x, y), su convolución se denota como:

$$g_r(x, y) = g(x, y) * h(x, y)$$

donde * representa la operación de convolución bidimensional. La Transformada de Fourier convierte esta operación en una simple multiplicación:

$$G_r(k_x, k_y) = G(k_x, k_y) \cdot H(k_x, k_y)$$

donde:

$$\begin{align*} G(k_x, k_y) & \quad \text{: transformada de Fourier de la imagen original}, \\ H(k_x, k_y) & \quad \text{: transformada de Fourier del kernel de filtrado}, \\ G_r(k_x, k_y) & \quad \text{: transformada de Fourier de la imagen resultante}. \end{align*}$$

Esta propiedad es especialmente útil porque muchas operaciones de convolución que en el dominio espacial requieren recorrer cada píxel y aplicar un kernel local, en el dominio de la frecuencia se reducen a multiplicaciones punto a punto entre matrices de la misma dimensión.

Cabe mencionar que, debido a la naturaleza discreta y finita de las imágenes digitales, esta relación se da en términos de convolución circular. Para que la equivalencia sea válida en la práctica, es necesario aplicar relleno con ceros (zero-padding) antes de transformar las imágenes, de modo que se eviten artefactos de aliasing o envolvimiento (wrapping) al realizar la multiplicación en frecuencia.

def ApplyFrequencyDomainFilter(image, kernel):
    """
    Applies a frequency domain filter to a grayscale image.

    This function computes the 2D Fourier Transform of the input image, applies the given filter 
    in the frequency domain, and then performs the inverse Fourier Transform to return the filtered image.

    Parameters:
    ----------
    image : np.ndarray
        Input grayscale image (2D numpy array).

    kernel : np.ndarray
        Frequency domain filter (2D numpy array) with the same shape as the input image.

    Returns:
    -------
    filtered_image : np.ndarray
        Filtered image (uint8) normalized to the range [0, 255].

    Notes:
    -----
    - The input image is assumed to be in grayscale format.
    - The kernel should be designed in the frequency domain and have the same dimensions as the input image.
    - The output image is normalized to ensure proper visualization.
    """
    # Compute the 2D Fourier Transform of the image
    f = np.fft.fft2(image)
    fshift = np.fft.fftshift(f)  # Shift zero frequency to the center

    # Apply the filter in the frequency domain
    filtered_freq = fshift * kernel

    # Compute the inverse Fourier Transform to return to the spatial domain
    temp = np.abs(np.fft.ifft2(np.fft.ifftshift(filtered_freq)))

    # Normalize the result to the range [0, 255] and convert to uint8
    filtered_image = cv.normalize(temp, None, 0, 255, cv.NORM_MINMAX)
    filtered_image = np.uint8(filtered_image)

    return filtered_image

Esta equivalencia no solo es una herramienta matemática elegante, sino que fundamenta toda una clase de técnicas de filtrado frecuencial utilizadas tanto en restauración como en mejoramiento de imágenes.


1.2. Ventajas del dominio frecuencial

El filtrado en el dominio de la frecuencia presenta ventajas claras sobre el filtrado espacial en ciertos contextos:

  • Diseño intuitivo de filtros: En el dominio espacial, los kernels de convolución pueden parecer arbitrarios o difíciles de interpretar. Por el contrario, en el dominio de la frecuencia, los filtros se diseñan directamente en función de las componentes espectrales que se desean atenuar o resaltar. Por ejemplo, un filtro pasa-bajos simplemente bloquea las frecuencias altas que corresponden a detalles finos o ruido, mientras deja pasar las bajas frecuencias responsables de las estructuras globales.

  • Eficiencia computacional con kernels grandes: Aunque la Transformada de Fourier y su inversa requieren procesamiento adicional, la Transformada Rápida de Fourier (FFT) permite implementaciones altamente eficientes. Cuando el kernel de convolución es grande, realizar una convolución directa en el dominio espacial tiene un costo computacional de Ο(N² M²) para una imagen de tamaño N × N y un kernel de tamaño M × M. En cambio, la transformación a frecuencia, multiplicación espectral y reconversión por FFT se realiza en Ο(N² log N), lo cual es más eficiente para kernels grandes.

Sin embargo, es importante notar que esta ventaja se diluye para kernels pequeños (por ejemplo, de 3x3 o 5x5), donde la convolución directa en el dominio espacial puede ser más rápida. Además, muchas aplicaciones modernas utilizan enfoques multiescala o convoluciones separables que reducen el costo espacial sin necesidad de transformarse al dominio frecuencial.

2. Tipos de Filtrado y Aplicaciones

El filtrado en el dominio de la frecuencia permite realizar tanto restauración como mejoramiento de imágenes. Dependiendo de cómo se diseñe la respuesta en frecuencia del filtro, se pueden atenuar detalles finos, resaltar bordes, eliminar ruido o incluso modificar propiedades de iluminación. A continuación se describen los tipos más comunes de filtros frecuenciales, sus fundamentos matemáticos y sus aplicaciones prácticas.

2.1. Restauración vs. Mejoramiento

Es importante distinguir entre dos objetivos principales del filtrado:

  • Restauración: Busca reconstruir la imagen original eliminando distorsiones o ruido que hayan degradado la calidad de la imagen. Este tipo de filtrado suele requerir una estimación del proceso de degradación (modelo del sistema de adquisición o transmisión) y es más común en aplicaciones científicas o médicas.

  • Mejoramiento (enhancement): No busca recuperar una "verdadera imagen", sino hacerla más útil para una tarea específica, como mejorar la visibilidad de bordes, estructuras o texturas. Es frecuente en aplicaciones de visión artificial y fotografía.

Ambos enfoques pueden beneficiarse del análisis en frecuencia, ya que permiten aislar las escalas o rangos espectrales relevantes a la tarea.


2.2. Filtros Pasa-Bajos (Lowpass)

Los filtros pasa-bajos permiten el paso de las bajas frecuencias y atenúan las altas. Su función principal es suavizar la imagen, reduciendo el ruido o eliminando detalles finos.

Filtro ideal

La versión más simple y teóricamente pura es el filtro pasa-bajos ideal, definido en el dominio de la frecuencia como:

$$H_{\text{ideal}}(f)| = \begin{cases} 1, & \text{si } |f| \leq f_c \\ 0, & \text{si } |f| > f_c \end{cases}$$

donde fc es la frecuencia de corte. Este filtro elimina completamente todas las frecuencias por encima de fc. Sin embargo, su implementación práctica es problemática: la transformada inversa de este filtro da lugar a una función sinc en el dominio espacial, que se extiende infinitamente y presenta efectos de oscilación (ringing) debido al fenómeno de Gibbs.

def CreateIdealLowpassFilter(shape, cutoff_frequency):
    """
    Creates an ideal low-pass filter kernel in the frequency domain.

    Parameters:
    ----------
    shape : tuple
        Shape of the filter (rows, cols), typically matching the image dimensions.
    cutoff_frequency : float
        Cutoff frequency for the low-pass filter.

    Returns:
    -------
    filter_kernel : np.ndarray
        Ideal low-pass filter kernel as a 2D numpy array.
    """
    rows, cols = shape
    crow, ccol = rows // 2, cols // 2
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**2 + (Y - crow)**2)

    # Ideal low-pass filter formula
    filter_kernel = distance <= cutoff_frequency
    return filter_kernel.astype(np.float32)

Filtro Gaussiano

Una alternativa más suave es el filtro gaussiano, cuya respuesta en frecuencia está dada por:

$$H_{\text{gauss}}(f)| = e^{-\frac{f^2}{2\sigma^2}}$$

Este filtro tiene la ventaja de no presentar ringing y de ser separable (puede aplicarse por filas y columnas), lo cual lo hace computacionalmente eficiente. Sin embargo, su desventaja es su transición suave, lo que implica un menor control sobre las frecuencias que se atenúan o conservan.

def CreateGaussianLowpassFilter(shape, cutoff_frequency):
    """
    Creates a Gaussian low-pass filter kernel in the frequency domain.

    Parameters:
    ----------
    shape : tuple
        Shape of the filter (rows, cols), typically matching the image dimensions.
    cutoff_frequency : float
        Cutoff frequency for the low-pass filter.

    Returns:
    -------
    filter_kernel : np.ndarray
        Gaussian low-pass filter kernel as a 2D numpy array.
    """
    rows, cols = shape
    crow, ccol = rows // 2, cols // 2
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**2 + (Y - crow)**2)

    # Gaussian low-pass filter formula
    filter_kernel = np.exp(-(distance**2) / (2 * (cutoff_frequency**2)))
    return filter_kernel.astype(np.float32)

Filtro de Butterworth

Este filtro busca un compromiso entre la transición abrupta del filtro ideal y la suavidad del gaussiano. Su respuesta está definida como:

$$H_{\text{bw}}(f)| = \frac{1}{1 + \left( \frac{f}{f_c} \right)^{2n}}$$

donde n es el orden del filtro, que controla la pendiente de la caída en la banda de transición. Es conocido como un filtro "máximamente plano", ya que no presenta ondulaciones (ripple) en la banda pasante.

def CreateButterworthLowpassFilter(shape, cutoff_frequency, order):
    """
    Creates a Butterworth low-pass filter kernel in the frequency domain.

    Parameters:
    ----------
    shape : tuple
        Shape of the filter (rows, cols), typically matching the image dimensions.
    cutoff_frequency : float
        Cutoff frequency for the low-pass filter.
    order : int
        Order of the Butterworth filter, controlling the sharpness of the transition.

    Returns:
    -------
    filter_kernel : np.ndarray
        Butterworth low-pass filter kernel as a 2D numpy array.
    """
    rows, cols = shape
    crow, ccol = rows // 2, cols // 2
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**2 + (Y - crow)**2)

    # Butterworth low-pass filter formula
    filter_kernel = 1 / (1 + (distance / (cutoff_frequency + 1e-5))**(2 * order))  # Avoid division by zero
    return filter_kernel.astype(np.float32)

Filtro de Lanczos

El filtro de Lanczos surge principalmente en tareas de reescalado de imágenes y reconstrucción, y está basado en una ventana truncada de la función sinc:

$$h(x) = \text{sinc}(x) \cdot \text{sinc}\left(\frac{x}{a}\right)$$

donde a es un parámetro que controla el ancho de la ventana. En frecuencia, ofrece una buena supresión de aliasing con una transición más nítida que el gaussiano, pero con menor ringing que el filtro ideal.

def CreateLanczosLowpassFilter(shape, cutoff_frequency, a=3):
    """
    Creates a Lanczos low-pass filter kernel in the frequency domain.

    Parameters:
    ----------
    shape : tuple
        Shape of the filter (rows, cols), typically matching the image dimensions.
    cutoff_frequency : float
        Frequency scaling factor (controls sharpness).
    a : int
        Lanczos window parameter (commonly 2 or 3). Larger values = narrower main lobe.

    Returns:
    -------
    filter_kernel : np.ndarray
        Lanczos low-pass filter kernel as a 2D numpy array.
    """
    rows, cols = shape
    crow, ccol = rows // 2, cols // 2

    # Coordenadas relativas al centro
    Y, X = np.ogrid[:rows, :cols]
    dx = X - ccol
    dy = Y - crow
    radius = np.sqrt(dx**2 + dy**2)

    # Normalizar el radio para hacerlo compatible con el parámetro a
    x = (radius / cutoff_frequency).astype(np.float32)

    # sinc(x) = sin(pi x) / (pi x), definida como 1 en x = 0
    def sinc(z):
        z = np.where(z == 0, 1e-8, z)  # evitar división por cero
        return np.sin(np.pi * z) / (np.pi * z)

    # Kernel de Lanczos en 2D: sinc(x) * sinc(x/a)
    lanczos_kernel = sinc(x) * sinc(x / a)

    # Forzar ceros fuera de la ventana a
    lanczos_kernel[x > a] = 0

    return lanczos_kernel.astype(np.float32)

2.3 Filtrado de Imágenes a Color en el Dominio de la Frecuencia

Al extender el filtrado en el dominio de la frecuencia a imágenes a color, una estrategia directa consiste en aplicar el mismo filtro frecuencial a cada canal (B, G, R) de forma independiente. La función ApplyFrequencyDomainFilterBGR realiza exactamente esto: procesa por separado cada componente de color, aplica el filtro en frecuencia y luego los vuelve a combinar. Aunque este método es sencillo y coherente con el filtrado en escala de grises, puede introducir ligeras distorsiones de color, ya que cada canal se modifica sin considerar su relación perceptual con los otros. Para evitar este problema, se pueden utilizar espacios de color alternativos como Lab o HSV, en los que se aplica el filtrado únicamente sobre el componente de luminancia, preservando así mejor la fidelidad del color original.

def ApplyFrequencyDomainFilterBGR(image_bgr, kernel):
    """
    Applies a frequency domain filter to each BGR channel of a color image independently.

    Parameters:
        image_bgr : np.ndarray
            Input color image (H x W x 3) in uint8 format.

        kernel : np.ndarray
            Frequency domain filter (2D array) of shape (H, W).

    Returns:
        np.ndarray:
            Filtered BGR image (uint8), same size as input.
    """
    if image_bgr.ndim != 3 or image_bgr.shape[2] != 3:
        raise ValueError("Input image must be BGR (H x W x 3).")

    filtered_channels = []
    for c in range(3):
        channel = image_bgr[:, :, c]
        filtered = ApplyFrequencyDomainFilter(channel, kernel)
        filtered_channels.append(filtered)

    return cv.merge(filtered_channels)

2.4. Filtros Pasa-Altos (Highpass)

Los filtros pasa-altos cumplen la función opuesta a los pasa-bajos: atenúan las bajas frecuencias (responsables de estructuras suaves y cambios graduales) y conservan o enfatizan las altas frecuencias, que suelen corresponder a bordes, texturas finas y detalles locales.

Una forma directa de obtener un filtro pasa-alto es restar un filtro pasa-bajo de una función constante:

$$H_{\text{highpass}}(f)| = 1 - |H_{\text{lowpass}}(f)$$

Esto permite generar versiones pasa-altas correspondientes a cualquier diseño pasa-bajo conocido, como los ideales, gaussianos o Butterworth.

Filtro Ideal

Similar al caso pasa-bajo, el filtro pasa-alto ideal se define como:

$$H_{\text{highpass}}(f)| = 1 - |H_{\text{lowpass}}(f)$$

En el dominio espacial, este filtro genera oscilaciones significativas (ringing) alrededor de los bordes y también se ve afectado por el fenómeno de Gibbs, por lo que rara vez se implementa directamente.

def CreateIdealHighpassFilter(shape, cutoff_frequency):
    """
    Creates an ideal high-pass filter kernel in the frequency domain.

    Parameters:
        shape : tuple
            Shape of the filter (rows, cols), typically matching the image dimensions.
        cutoff_frequency : float
            Cutoff frequency for the high-pass filter.

    Returns:
        filter_kernel : np.ndarray
            Ideal high-pass filter kernel as a 2D numpy array.
    """

    rows, cols = shape
    crow, ccol = rows // 2, cols // 2
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**2 + (Y - crow)**2)

    # Ideal high-pass filter formula
    filter_kernel = distance > cutoff_frequency
    return filter_kernel.astype(np.float32)

Filtro Gaussiano

La versión pasa-alta del filtro gaussiano se obtiene de manera complementaria:

$$H_{\text{gauss-high}}(f)| = 1 - e^{-\frac{f^2}{2\sigma^2}}$$

Este filtro es útil para detectar bordes suaves o graduales, especialmente cuando se requiere evitar artefactos de sobre-resaltado.

def CreateGaussianHighpassFilter(shape, cutoff_frequency):
    """
    Creates a Gaussian high-pass filter kernel in the frequency domain.

    Parameters:
        shape : tuple
            Shape of the filter (rows, cols), typically matching the image dimensions.
        cutoff_frequency : float
            Cutoff frequency for the high-pass filter.

    Returns:
        filter_kernel : np.ndarray
            Gaussian high-pass filter kernel as a 2D numpy array.
    """
    rows, cols = shape
    crow, ccol = rows // 2, cols // 2
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**2 + (Y - crow)**2)

    # Gaussian high-pass filter formula
    lowpass_kernel = np.exp(-(distance**2) / (2 * (cutoff_frequency**2)))
    highpass_kernel = 1 - lowpass_kernel
    return highpass_kernel.astype(np.float32)

Filtro de Butterworth

El filtro pasa-alto de Butterworth se define como:

$$H_{\text{bw}}(f)| = \frac{1}{1 + \left( \frac{f_c}{f} \right)^{2n}}$$

Este diseño, con su control por orden n, permite ajustar finamente el compromiso entre nitidez y estabilidad numérica. A diferencia del filtro ideal, el de Butterworth presenta una transición continua y suave, evitando oscilaciones excesivas.

def CreateButterworthHighpassFilter(shape, cutoff_frequency, order):
    """
    Creates a Butterworth high-pass filter kernel in the frequency domain.

    Parameters:
        shape : tuple
            Shape of the filter (rows, cols), typically matching the image dimensions.
        cutoff_frequency : float
            Cutoff frequency for the high-pass filter.
        order : int
            Order of the Butterworth filter, controlling the sharpness of the transition.

    Returns:
        filter_kernel : np.ndarray
            Butterworth high-pass filter kernel as a 2D numpy array.
    """
    rows, cols = shape
    crow, ccol = rows // 2, cols // 2
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**2 + (Y - crow)**2)

    # Butterworth high-pass filter formula
    filter_kernel = 1 / (1 + (cutoff_frequency / (distance + 1e-5))**(2 * order))  # Avoid division by zero
    return filter_kernel.astype(np.float32)

Aplicaciones

Los filtros pasa-altos son esenciales en tareas como:

  • Detección de bordes: Las transiciones abruptas en la intensidad de la imagen se traducen en frecuencias altas, que estos filtros conservan o acentúan. Aunque los métodos espaciales como Sobel o Laplaciano son comunes, los enfoques frecuenciales permiten un control más preciso de la respuesta espectral.

  • Realce de detalles (Detail Enhancement): Al aplicar un filtro pasa-alto y sumarlo nuevamente a la imagen original, se pueden enfatizar detalles sin perder información global. Esta técnica es la base del enmascaramiento no agudo y otras estrategias de mejora visual.

2.5. Filtros Pasa-Banda (Bandpass)

Los filtros pasa-banda están diseñados para aislar un rango específico de frecuencias, bloqueando tanto las bajas como las altas. Son útiles cuando se desea conservar estructuras que se encuentren a una escala intermedia, ignorando patrones demasiado gruesos o demasiado finos.

Una implementación clásica de filtro pasa-banda se logra como la diferencia entre un filtro pasa-bajo y un filtro pasa-alto con distintas frecuencias de corte. Pero existen también filtros diseñados directamente para resaltar componentes de ciertas frecuencias, como el Laplaciano de Gaussiano y técnicas de enmascaramiento no agudo.


Filtro Laplaciano de Gaussiano (LoG)

El filtro LoG surge de la combinación de dos operaciones: suavizado mediante un filtro gaussiano, seguido por la aplicación del operador Laplaciano (segunda derivada). Aunque originalmente se define en el dominio espacial, también tiene una expresión directa en frecuencia:

$$H_{\text{LoG}}(f)| = -f^2 \cdot e^{-\frac{f^2}{2f_c^2}}$$

Este filtro actúa como un detector de bordes, pero a diferencia de los filtros pasa-altos convencionales, resalta frecuencias intermedias y suprime tanto bajas como altas. De ahí su naturaleza pasa-banda.

  • La parte f² enfatiza componentes de frecuencia creciente.

  • El término exponencial atenúa frecuencias más allá del umbral fc.

Este filtro también es isotrópico (invariante a rotaciones) y ampliamente utilizado en visión por computadora y reconocimiento de patrones, como en la detección de blobs.

def CreateLaplacianOfGaussianFilter(shape, cutoff_freq):
    """
    Create a Laplacian of Gaussian (LoG) filter in the frequency domain.

    Parameters:
        shape        : tuple, (height, width) of the image
        cutoff_freq  : float, frequency cutoff (f_c) that controls the Gaussian spread

    Returns:
        log_filter   : 2D numpy array with the filter in the frequency domain
    """
    rows, cols = shape
    cy, cx = rows // 2, cols // 2

    # Create frequency grids centered at (0,0)
    u = np.fft.fftfreq(cols).reshape(1, -1)
    v = np.fft.fftfreq(rows).reshape(-1, 1)

    # Shift the frequency grids so that (0,0) is at the center
    u = np.fft.fftshift(u)
    v = np.fft.fftshift(v)

    # Compute squared frequency radius: f^2 = u^2 + v^2
    f_squared = u**2 + v**2

    # Laplacian of Gaussian filter in frequency domain
    log_filter = -4 * (np.pi**2) * f_squared * np.exp(-f_squared / (2 * (cutoff_freq ** 2)))

    return log_filter

Enmascaramiento no agudo (Unsharp Masking)

A pesar de su nombre, el enmascaramiento no agudo (unsharp masking) es una técnica para agudizar (realzar) los detalles de una imagen. Su funcionamiento se basa en extraer las componentes de alta frecuencia y sumarlas de nuevo a la imagen original:

$$g_{\text{realzada}}(x, y) = g(x, y) + \alpha \cdot \left[g(x, y) - g_{\text{suavizada}}(x, y)\right]$$

Este esquema puede verse como:

$$g_{\text{realzada}} = (1 + \alpha) \cdot g - \alpha \cdot (g * h)$$

donde h es un filtro pasa-bajo (por ejemplo, Gaussiano) y α un parámetro de realce.

En frecuencia, esta operación corresponde a aplicar un filtro con respuesta:

$$H_{\text{unsharp}}(f) = 1 + \alpha \cdot \left[1 - H_{\text{lowpass}}(f)\right]$$

Esto da como resultado una respuesta pasa-banda modificada, que enfatiza un rango intermedio de frecuencias con ganancia ajustable.

  • Ventajas: control preciso sobre el nivel de realce.

  • Aplicaciones: mejora de detalles en imágenes médicas, documentos escaneados, o fotografía digital.

def CreateUnsharpMaskingFilter(shape, cutoff_freq, alpha=1.0, method='gaussian'):
    """
    Create an unsharp masking filter in the frequency domain.

    Parameters:
        shape        : tuple, (height, width) of the image
        cutoff_freq  : float, cutoff frequency for the lowpass component
        alpha        : float, sharpening factor (>0)
        method       : str, type of lowpass ('gaussian', 'ideal', 'butterworth')

    Returns:
        H_unsharp    : 2D numpy array with the unsharp masking filter
    """
    if method == 'gaussian':
        H_lowpass = CreateGaussianLowpassFilter(shape, cutoff_freq)
    elif method == 'ideal':
        H_lowpass = CreateIdealLowpassFilter(shape, cutoff_freq)
    elif method == 'butterworth':
        H_lowpass = CreateButterworthLowpassFilter(shape, cutoff_freq, order=2)
    else:
        raise ValueError("Unsupported method. Choose 'gaussian', 'ideal', or 'butterworth'.")

    # Unsharp masking filter: H_unsharp(f) = 1 + alpha * (1 - H_lowpass(f))
    H_unsharp = 1 + alpha * (1 - H_lowpass)

    return H_unsharp

2.6. Filtrado Homomórfico

En muchas imágenes del mundo real, especialmente en condiciones de iluminación natural, la intensidad registrada por el sensor es una combinación de dos factores principales:

  • Iluminación (L(x, y)): una componente de variación lenta, asociada con las condiciones externas de luz, sombras suaves y gradientes globales.

  • Reflectancia (R(x, y)): una componente de variación rápida, relacionada con los detalles locales, texturas y estructuras intrínsecas de la escena.

El modelo multiplicativo que describe esta relación es:

$$E(x, y) = L(x, y) \cdot R(x, y)$$

Este modelo es problemático para el análisis frecuencial directo, ya que la Transformada de Fourier no maneja productos de funciones de forma directa. Para resolver esto, el filtrado homomórfico transforma el modelo multiplicativo en uno aditivo mediante un logaritmo:

$$log E(x, y) = \log L(x, y) + \log R(x, y)$$

Una vez en esta forma, es posible aplicar un filtro frecuencial que suprima las bajas frecuencias (iluminación) y preserve o realce las altas (reflectancia). Típicamente, se utiliza un filtro pasa-alto suave o un filtro pasa-banda con énfasis controlado.

Proceso general:

  1. Aplicar logaritmo a la imagen:

    $$s(x, y) = \log E(x, y)$$

  2. Transformar al dominio de la frecuencia:

    $$S(k_x, k_y) = \mathcal{F}{s(x, y)}$$

  3. Aplicar un filtro H(kₓ , ky) que atenúe las bajas frecuencias:

    $$S_r(k_x, k_y) = S(k_x, k_y) \cdot H(k_x, k_y)$$

  4. Transformar de regreso al dominio espacial e invertir el logaritmo:

    $$E_r(x, y) = \exp\left(\mathcal{F}^{-1}{S_r(k_x, k_y)}\right)$$

Resultados y aplicaciones

  • Realce de detalles en sombras: al atenuar la iluminación global, se pueden destacar detalles que de otro modo quedarían ocultos.

  • Compensación de iluminación desigual: muy útil en imágenes médicas, fotografía artística y escaneos de documentos.

  • Preprocesamiento para segmentación: al normalizar variaciones de iluminación, se mejora la robustez de algoritmos posteriores.

La clave del filtrado homomórfico está en elegir adecuadamente el filtro H(kₓ, ky), que suele tener forma de filtro pasa-alto modulado:

$$H(k_x, k_y) = (\gamma_H - \gamma_L) \cdot \left[1 - e^{-\frac{(k_x^2 + k_y^2)}{2\sigma^2}}\right] + \gamma_L$$

donde:

$$\begin{align*} \gamma_L &< 1 \quad \text{: controla el nivel de atenuación de la iluminación}, \\ \gamma_H &> 1 \quad \text{: define el realce de detalles}, \\ \sigma &\quad \text{: regula la transición entre bandas}. \end{align*}$$

def HomomorphicFilterLab(bgr_img: np.ndarray, gammaL=0.5, gammaH=1.5, sigma=30) -> np.ndarray:
    """
    Applies homomorphic filtering to the L (lightness) channel of a BGR image using the CIELAB color space.

    Parameters:
        bgr_img : np.ndarray
            Input image in BGR format (as used by OpenCV), with dtype uint8 and shape (H, W, 3).
        gammaL : float
            Gain for low frequencies (<1, suppresses illumination).
        gammaH : float
            Gain for high frequencies (>1, enhances details).
        sigma : float
            Controls the transition between low and high frequencies.

    Returns:
        np.ndarray:
            BGR image after homomorphic filtering on the luminance channel (dtype uint8, same shape as input).
    """

    # Convert to LAB color space
    lab = cv.cvtColor(bgr_img, cv.COLOR_BGR2LAB)
    l, a, b = cv.split(lab)

    # Convert L to float32 and scale to [0, 255] if necessary (OpenCV stores L in [0, 255] already)
    l_float = l.astype(np.float32)

    # Step 1: Log-transform
    log_l = np.log1p(l_float)

    # Step 2: DFT (centered)
    dft = np.fft.fft2(log_l)
    dft_shift = np.fft.fftshift(dft)

    # Step 3: Homomorphic filter in frequency domain
    rows, cols = l.shape
    u = np.arange(-cols//2, cols//2)
    v = np.arange(-rows//2, rows//2)
    U, V = np.meshgrid(u, v)
    D2 = U**2 + V**2
    H = (gammaH - gammaL) * (1 - np.exp(-D2 / (2 * sigma**2))) + gammaL

    # Step 4: Apply filter
    filtered_dft = dft_shift * H

    # Step 5: Inverse DFT
    inv_dft = np.fft.ifft2(np.fft.ifftshift(filtered_dft))
    inv_dft = np.real(inv_dft)

    # Step 6: Inverse log
    l_filtered = np.expm1(inv_dft)

    # Normalize and clip to [0, 255]
    l_filtered = np.clip(l_filtered, 0, 255).astype(np.uint8)

    # Merge back and convert to BGR
    lab_filtered = cv.merge([l_filtered, a, b])
    bgr_result = cv.cvtColor(lab_filtered, cv.COLOR_LAB2BGR)

    return bgr_result

3. Limitaciones y Alternativas

Aunque el filtrado en el dominio de la frecuencia ofrece herramientas poderosas para modificar, restaurar o mejorar imágenes, no está exento de limitaciones. En esta sección se abordan los principales desafíos asociados a esta técnica, así como algunas alternativas o extensiones que intentan superarlos.

3.1. Límite de Gabor y Localización Tiempo-Frecuencia

Un principio fundamental en el análisis de señales es que no se puede lograr simultáneamente una alta resolución en el dominio espacial y en el dominio frecuencial. Esta idea se expresa en el llamado límite de Gabor, una manifestación de la desigualdad de Heisenberg adaptada al procesamiento de señales:

$$\sigma_x \cdot \sigma_f \geq \frac{1}{4\pi}$$

donde:

$$\begin{align*} \sigma_x &= \text{Dispersión (ancho) en el dominio espacial}, \\ \sigma_f &= \text{Dispersión en el dominio de la frecuencia}. \end{align*}$$

Este límite implica que al utilizar la Transformada de Fourier clásica, se pierde completamente la información de localización espacial: sabemos qué frecuencias están presentes, pero no dónde ocurren. Esto es suficiente para imágenes globalmente estacionarias, pero no para patrones locales o texturas que cambian en distintas regiones.

Ejemplo ilustrativo:

En procesamiento de audio, una señal musical puede analizarse en frecuencia, pero con la Transformada de Fourier tradicional no se puede saber cuándo suenan ciertas notas, solo que están presentes. En imágenes, ocurre algo similar: podemos detectar ciertas frecuencias, pero no identificar en qué zonas específicas se encuentran.

Alternativas:

Para superar esta limitación, se han desarrollado métodos que permiten un compromiso mejor entre resolución espacial y frecuencial. Algunas de las alternativas más relevantes son:

  • Transformada de Fourier de ventana (STFT): Aplica la transformada a segmentos locales de la imagen, lo que permite una cierta localización espacial. Sin embargo, la resolución está limitada por el tamaño fijo de la ventana.

  • Transformadas multiescala (como wavelets): Proveen una descomposición jerárquica con mejor adaptabilidad. Las wavelets permiten una buena localización espacial para frecuencias altas y una buena localización frecuencial para frecuencias bajas, lo cual es ideal para imágenes con estructuras a múltiples escalas.

  • Filtros de Gabor: Son versiones localizadas en espacio y frecuencia de la transformada de Fourier. Ofrecen una excelente representación para texturas y patrones periódicos en distintas orientaciones y escalas, a costa de mayor complejidad computacional.

Conclusión

El filtrado en el dominio de la frecuencia ofrece una perspectiva poderosa y elegante para el procesamiento de imágenes. A diferencia de los métodos espaciales, permite analizar y modificar el contenido de una imagen según la escala y complejidad de sus estructuras internas, revelando patrones que no siempre son evidentes en el dominio de los píxeles.

A lo largo del artículo hemos visto cómo, gracias al teorema de la convolución circular, es posible transformar una operación costosa como la convolución espacial en una multiplicación eficiente en frecuencia. Esto abre la puerta a filtros intuitivos, como los pasa-bajos, pasa-altos, pasa-banda y homomórficos, cada uno con propiedades específicas y aplicaciones particulares.

También se destacó que, si bien el dominio frecuencial permite diseñar filtros globales con mayor control y eficiencia para kernels grandes, presenta limitaciones inherentes en la localización espacial. Por ello, en escenarios donde el contexto local es crítico —como en el análisis de texturas o en imágenes no estacionarias—, conviene considerar enfoques híbridos o alternativos como los filtros de Gabor o las transformadas wavelet.

El dominio de la frecuencia no solo amplía el repertorio de herramientas disponibles en el procesamiento de imágenes, sino que ofrece un marco teórico profundo para entender cómo fluye la información visual en distintos niveles de escala y complejidad. Conocerlo, y saber cuándo aplicarlo, es esencial para el diseño de sistemas robustos de análisis visual, compresión, restauración y mejora de calidad.

0
Subscribe to my newsletter

Read articles from Francisco Zavala directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Francisco Zavala
Francisco Zavala