Skip to main content

Command Palette

Search for a command to run...

Redimensionamiento de imágenes. técnicas clásicas de interpolación

Updated
14 min read
Redimensionamiento de imágenes. técnicas clásicas de interpolación

Introducción

El redimensionamiento de imágenes consiste en cambiar las dimensiones de una imagen, ya sea para aumentar su tamaño (upscaling) o reducirlo (downscaling), manteniendo la mayor fidelidad posible respecto a la imagen original.Este proceso es ampliamente usado, tanto en la edición de fotos y videos como en aplicaciones de procesamiento de imágenes como reconocimiento facial, visión robótica, y entrenamiento de modelos de inteligencia artificial, donde la coherencia y la calidad de los datos visuales son determinantes.

Aunque la idea parece sencilla —simplemente ampliar o reducir una imagen—, esto supone un desafío técnico: cada píxel agregado o eliminado debe generarse de manera que respete la estructura visual, así como los bordes. De lo contrario, pueden aparecer artefactos como pixelado, borrosidad o aliasing, afectando tanto la percepción visual como el rendimiento de algoritmos posteriores.

Existen múltiples métodos de redimensionamiento, cada uno con sus ventajas y limitaciones, que se diferencian principalmente por la manera en que calculan los nuevos valores de pixel a partir de los existentes. En este artículo exploraremos cuatro técnicas ampliamente utilizadas: Vecino más cercano (Nearest Neighbor), Interpolación bilineal, Interpolación bicúbica y Lanczos, analizando cómo funcionan, sus ventajas, desventajas y resultados prácticos sobre imágenes reales.

1. Conceptos básicos

Antes de profundizar en los métodos de redimensionamiento, es importante entender algunos conceptos fundamentales que determinan cómo y por qué se transforman las imágenes.

1.1 Píxeles y resolución

Una imagen digital está compuesta por píxeles, que son los elementos mínimos de información visual. Cada píxel contiene información de color y, en conjunto, forman la imagen completa. La resolución de una imagen se define por la cantidad de píxeles en sus dimensiones: ancho × alto. Por ejemplo, una imagen de 800×600 píxeles tiene 480,000 píxeles en total.

Cambiar la resolución de una imagen implica recalcular la información de los píxeles para ajustarla a las nuevas dimensiones.

1.2 Upscaling y downscaling

  • Upscaling: aumentar el tamaño de la imagen. Esto requiere generar nuevos píxeles a partir de los existentes. El desafío principal es preservar detalles y evitar que la imagen se vea borrosa o pixelada.

  • Downscaling: reducir el tamaño de la imagen. Aquí se deben combinar o eliminar píxeles de manera que la información relevante se mantenga y no se pierdan detalles importantes.

Cada operación tiene implicaciones diferentes sobre la calidad de la imagen y sobre el rendimiento de los algoritmos que la procesarán.

1.3. Problemas comunes

Al redimensionar imágenes, es habitual enfrentarse a ciertos artefactos:

  • Pixelado: ocurre cuando se usan métodos muy simples, como el vecino más cercano, y los bordes aparecen escalonados.

  • Borroso o suavizado excesivo: puede aparecer al usar interpolaciones que promedian demasiado los píxeles cercanos, como bilineal o bicúbica.

  • Aliasing: sucede cuando se reduce demasiado la resolución y se pierden detalles finos, generando patrones no deseados en la imagen.

Comprender estos problemas permite elegir el método de redimensionamiento más adecuado según el objetivo, ya sea velocidad, fidelidad visual o preservación de detalles.

2. Vecino más cercano (Nearest Neighbor)

El método de vecino más cercano es la forma más simple de redimensionar imágenes. Su funcionamiento es intuitivo: cada píxel de la nueva imagen toma el valor del píxel más cercano de la imagen original, sin realizar ningún tipo de interpolación. Esto significa que no se generan valores intermedios; simplemente se copia la información existente.

Ventajas:

  • Muy rápido y eficiente.

  • Fácil de implementar y comprender.

Desventajas:

  • Produce bordes escalonados o pixelados, especialmente al aumentar la imagen.

  • No preserva detalles finos ni suaviza transiciones de color.

Implementación en Python:

def resize_nearest_neighbor(image, new_width, new_height):
    """
    Redimensiona una imagen usando interpolación Nearest Neighbor (vecino más cercano).

    :param image: imagen de entrada (numpy array)
    :param new_width: ancho deseado
    :param new_height: alto deseado
    :return: imagen redimensionada
    """
    h_in, w_in = image.shape[:2]
    h_out, w_out = new_height, new_width

    # Crear imagen vacía de salida
    if len(image.shape) == 3:  # Imagen en color
        output = np.zeros((h_out, w_out, image.shape[2]), dtype=image.dtype)
    else:  # Imagen en escala de grises
        output = np.zeros((h_out, w_out), dtype=image.dtype)

    # Escalamiento
    for y_out in range(h_out):
        for x_out in range(w_out):
            # Mapeo inverso (salida → entrada)
            x_in = int(round(x_out * w_in / w_out))
            y_in = int(round(y_out * h_in / h_out))

            # Clamping para evitar índices fuera de rango
            x_in = min(w_in - 1, x_in)
            y_in = min(h_in - 1, y_in)

            output[y_out, x_out] = image[y_in, x_in]

    return output

Explicación del algoritmo:

  1. Se determina el tamaño de la imagen original (h_in, w_in) y el tamaño deseado (h_out, w_out).

  2. Se crea una matriz vacía para la imagen de salida.

  3. Para cada píxel de la imagen de salida, se calcula la posición correspondiente en la imagen original mediante un mapeo inverso.

  4. Se aplica un clamping para evitar acceder fuera del rango de la imagen original.

  5. Se copia el valor del píxel más cercano al nuevo píxel.

2. Interpolación bilineal (Bilinear)

La interpolación bilineal es un método más avanzado que el de vecino más cercano, ya que calcula cada nuevo píxel como una combinación ponderada de los cuatro píxeles más cercanos de la imagen original. En lugar de copiar un único valor, el algoritmo realiza un promedio lineal en ambas direcciones —horizontal y vertical—, lo que produce transiciones más suaves y bordes menos marcados.

En términos geométricos, puede interpretarse como una interpolación primero a lo largo del eje x (entre los píxeles vecinos horizontales) y luego a lo largo del eje y (entre los resultados de esas interpolaciones).

Ventajas:

  • Genera resultados más suaves y agradables visualmente.

  • Reduce el efecto de pixelado presente en el método de vecino más cercano.

Desventajas:

  • Puede producir una ligera pérdida de nitidez.

  • Requiere más cálculos, por lo que es más lento.

Implementación en Python

def resize_bilinear(image, new_width, new_height):
    """
    Redimensiona una imagen usando interpolación bilineal.

    :param image: Imagen de entrada (numpy array)
    :param new_width: Ancho de salida
    :param new_height: Alto de salida
    :return: Imagen redimensionada
    """
    h_in, w_in = image.shape[:2]
    h_out, w_out = new_height, new_width

    # Imagen de salida
    if len(image.shape) == 3:
        output = np.zeros((h_out, w_out, image.shape[2]), dtype=np.float32)
    else:
        output = np.zeros((h_out, w_out), dtype=np.float32)

    for y_out in range(h_out):
        for x_out in range(w_out):
            # Mapear coordenada de salida a entrada (espacio continuo)
            x_in = (x_out + 0.5) * (w_in / w_out) - 0.5
            y_in = (y_out + 0.5) * (h_in / h_out) - 0.5

            x1 = int(np.floor(x_in))
            y1 = int(np.floor(y_in))
            x2 = min(x1 + 1, w_in - 1)
            y2 = min(y1 + 1, h_in - 1)

            a = x_in - x1
            b = y_in - y1

            # Píxeles vecinos
            Q11 = image[y1, x1]
            Q21 = image[y1, x2]
            Q12 = image[y2, x1]
            Q22 = image[y2, x2]

            # Interpolación bilineal
            output[y_out, x_out] = (1 - a) * (1 - b) * Q11 + \
                                   a * (1 - b) * Q21 + \
                                   (1 - a) * b * Q12 + \
                                   a * b * Q22

    return np.clip(output, 0, 255).astype(np.uint8)

Explicación paso a paso

  1. Mapeo inverso:
    Cada coordenada de la imagen de salida se transforma a una posición continua en la imagen original (x_in, y_in).
    Este mapeo asegura que cada píxel de la salida “corresponda” a una ubicación en la entrada.

  2. Selección de píxeles vecinos:
    Se identifican los cuatro píxeles más cercanos a esa posición continua:

    • Q11 = esquina superior izquierda

    • Q21 = esquina superior derecha

    • Q12 = esquina inferior izquierda

    • Q22 = esquina inferior derecha

  3. Interpolación en dos direcciones:

    • Primero se interpola horizontalmente entre Q11 y Q21, y entre Q12 y Q22.

    • Luego se interpola verticalmente entre los resultados anteriores.

  4. Normalización y clipping:
    Los valores resultantes se limitan al rango [0, 255] para mantener la validez de los niveles de intensidad.

Al comparar la imagen original con la redimensionada mediante interpolación bilineal, se observa una suavidad mayor respecto al método de vecino más cercano. Sin embargo, los bordes pueden perder algo de nitidez, especialmente cuando se amplía la imagen.

Este método es muy usado en aplicaciones donde se busca un equilibrio entre velocidad y calidad visual, como en la visualización de imágenes en tiempo real o el preprocesamiento para modelos de aprendizaje profundo.

3. Interpolación bicúbica (Bicubic)

La interpolación bicúbica es una extensión de la bilineal que utiliza 16 píxeles vecinos (una ventana de 4×4) para estimar el valor de cada nuevo píxel. En lugar de promediar linealmente, este método aplica una función cúbica para calcular los pesos de cada píxel según su distancia a la posición interpolada.

Este enfoque fue propuesto por Robert G. Keys en 1981, y su función de peso es una aproximación suave y continua que logra transiciones más naturales entre los píxeles. El resultado es una imagen más nítida y con mejor preservación de detalles, especialmente al realizar upscaling.

Implementación en Python

def cubic_weight(x, a=-0.5):
    """Función de pesos cúbica (Keys, 1981)"""
    x = abs(x)
    if x < 1:
        return (a + 2) * (x ** 3) - (a + 3) * (x ** 2) + 1
    elif x < 2:
        return a * (x ** 3) - (5 * a) * (x ** 2) + (8 * a) * x - 4 * a
    else:
        return 0

def resize_bicubic(image, new_width, new_height):
    """
    Redimensiona una imagen usando interpolación bicúbica.

    :param image: Imagen de entrada (numpy array)
    :param new_width: Ancho de salida
    :param new_height: Alto de salida
    :return: Imagen redimensionada
    """
    h_in, w_in = image.shape[:2]
    h_out, w_out = new_height, new_width

    # Imagen de salida
    if len(image.shape) == 3:
        output = np.zeros((h_out, w_out, image.shape[2]), dtype=np.float32)
    else:
        output = np.zeros((h_out, w_out), dtype=np.float32)

    scale_x = w_in / w_out
    scale_y = h_in / h_out

    for y_out in range(h_out):
        for x_out in range(w_out):
            # Posición en la imagen original
            x_in = (x_out + 0.5) * scale_x - 0.5
            y_in = (y_out + 0.5) * scale_y - 0.5

            x_base = int(np.floor(x_in))
            y_base = int(np.floor(y_in))

            value = np.zeros(image.shape[2], dtype=np.float32) if len(image.shape) == 3 else 0.0

            # Recorrer vecinos 4x4
            for m in range(-1, 3):
                for n in range(-1, 3):
                    x_idx = min(max(x_base + n, 0), w_in - 1)
                    y_idx = min(max(y_base + m, 0), h_in - 1)

                    wx = cubic_weight(x_in - (x_base + n))
                    wy = cubic_weight(y_in - (y_base + m))
                    w = wx * wy

                    value += image[y_idx, x_idx] * w

            output[y_out, x_out] = value

    return np.clip(output, 0, 255).astype(np.uint8)

Explicación del algoritmo

  1. Función de peso cúbica (cubic_weight)
    Define cómo contribuye cada píxel vecino en función de su distancia.

    • El parámetro a controla la forma del kernel (por defecto a = -0.5, conocido como Bicubic de Catmull-Rom).

    • Si x < 1, el peso es mayor (vecinos más cercanos).

    • Si 1 ≤ x < 2, el peso decae suavemente.

    • Si x ≥ 2, el peso es cero (no contribuye).

  2. Mapeo inverso y escalado:
    Cada coordenada de salida (x_out, y_out) se mapea a una posición flotante en la imagen original (x_in, y_in), manteniendo las proporciones.

  3. Ventana de interpolación 4×4:
    Se toman 16 píxeles vecinos alrededor de la posición mapeada.
    Para cada uno, se calculan los pesos cúbicos wx y wy y su producto w = wx * wy.

  4. Suma ponderada:
    Cada píxel vecino contribuye proporcionalmente a su peso.
    El resultado final se limita con np.clip() al rango [0, 255] para asegurar valores válidos de intensidad.

Características y efectos visuales

  • Suavidad: las transiciones de color son más naturales que con interpolación bilineal.

  • Nitidez: mantiene mejor los bordes y detalles finos, evitando el efecto borroso.

  • Costo computacional: más lento, ya que se evalúan 16 píxeles por cada nuevo píxel.

Bilineal (izq.) · Bicúbica (centro)

Este método es ideal cuando se busca una alta calidad visual, por ejemplo, en procesamiento fotográfico o en tareas donde la textura y el detalle son importantes.


4. Interpolación Lanczos

La interpolación Lanczos es una de las técnicas más precisas y sofisticadas para el redimensionamiento de imágenes. Está basada en la función sinc, la cual representa el filtro de reconstrucción ideal en el dominio de la frecuencia. En teoría, este filtro preserva toda la información posible durante el reescalado, evitando aliasing y manteniendo los bordes definidos.

Sin embargo, la función sinc tiene soporte infinito, lo que la hace impráctica para implementaciones reales. Por ello, Lanczos propuso una versión ventaneada de la sinc —es decir, truncada mediante otra sinc— que conserva sus propiedades principales pero con un alcance finito controlado por el parámetro a.

Fundamento teórico

El kernel de Lanczos se define como:

$$L(x) = \begin{cases} sinc(x) \cdot sinc\!\left(\dfrac{x}{a}\right), & |x| < a \\[8pt] 0, & \text{en otro caso} \end{cases}$$

donde a (comúnmente 2 o 3) determina el tamaño del soporte:

  • Un valor mayor de a produce resultados más suaves y detallados, pero aumenta el costo computacional.

  • Un valor menor reduce el costo, pero puede generar aliasing.

La multiplicación de dos funciones sinc actúa como una ventana suavizadora, que atenúa las oscilaciones de alta frecuencia y evita los artefactos típicos de los métodos más simples.


Implementación en Python

def sinc(x):
    return np.sinc(x)  # np.sinc ya incluye π, usa sin(πx)/(πx)

def lanczos_kernel(a=3, size=1000):
    """Precalcula el kernel de Lanczos en un rango continuo"""
    x = np.linspace(-a+1, a-1, size)
    k = sinc(x) * sinc(x / a)
    k[np.abs(x) >= a] = 0
    return k

def resize_lanczos_fast(image, new_width, new_height, a=3):
    h_in, w_in = image.shape[:2]
    h_out, w_out = new_height, new_width

    scale_x = w_in / w_out
    scale_y = h_in / h_out

    # Precalcular posiciones en la imagen original
    x_coords = (np.arange(w_out) + 0.5) * scale_x - 0.5
    y_coords = (np.arange(h_out) + 0.5) * scale_y - 0.5

    # Precalcular los índices y pesos para X
    x_idx = np.floor(x_coords).astype(int)
    x_weights = np.zeros((w_out, 2*a))
    for i, xc in enumerate(x_coords):
        for n in range(-a+1, a+1):
            idx = min(max(x_idx[i] + n, 0), w_in - 1)
            x_weights[i, n + a - 1] = sinc(xc - (x_idx[i] + n)) * sinc((xc - (x_idx[i] + n)) / a)

    # Normalizar pesos
    x_weights /= np.sum(x_weights, axis=1, keepdims=True)

    # Paso 1: interpolación horizontal
    tmp = np.zeros((h_in, w_out, image.shape[2]), dtype=np.float32)
    for i in range(w_out):
        for n in range(-a+1, a+1):
            idx = np.clip(x_idx[i] + n, 0, w_in-1)
            tmp[:, i] += image[:, idx] * x_weights[i, n + a - 1]

    # Precalcular los índices y pesos para Y
    y_idx = np.floor(y_coords).astype(int)
    y_weights = np.zeros((h_out, 2*a))
    for j, yc in enumerate(y_coords):
        for m in range(-a+1, a+1):
            idy = min(max(y_idx[j] + m, 0), h_in - 1)
            y_weights[j, m + a - 1] = sinc(yc - (y_idx[j] + m)) * sinc((yc - (y_idx[j] + m)) / a)

    # Normalizar pesos
    y_weights /= np.sum(y_weights, axis=1, keepdims=True)

    # Paso 2: interpolación vertical
    output = np.zeros((h_out, w_out, image.shape[2]), dtype=np.float32)
    for j in range(h_out):
        for m in range(-a+1, a+1):
            idy = np.clip(y_idx[j] + m, 0, h_in-1)
            #output[j] += tmp[idy, :, :] * y_weights[j, m + a - 1][:, None]
            output[j] += tmp[idy, :, :] * y_weights[j, m + a - 1]


    return np.clip(output, 0, 255).astype(np.uint8)

Explicación del algoritmo

  1. Función sinc y ventana:
    La función np.sinc(x) ya implementa la forma sin(πx)/(πx)..
    El kernel de Lanczos se obtiene multiplicando dos sinc: una principal y otra escalada por a.

  2. Separabilidad:
    La interpolación Lanczos es separable, por lo que se puede aplicar primero en la dirección x y luego en y.
    Esto reduce drásticamente el costo computacional de una versión completamente 2D.

  3. Pesos precalculados:
    Para optimizar el cálculo, los pesos de interpolación se precalculan tanto para las coordenadas horizontales como verticales, normalizándose después para conservar energía (sumen 1).

  4. Interpolación en dos pasos:

    • Paso horizontal: se genera una imagen temporal tmp interpolando a lo largo de las columnas.

    • Paso vertical: se interpola tmp a lo largo de las filas para obtener la imagen final.


Características visuales y rendimiento

  • Calidad sobresaliente: preserva detalles, bordes y textura con mínima pérdida visual.

  • Sin aliasing: evita artefactos comunes en reducciones de tamaño.

  • Costo computacional alto: debido a los cálculos del kernel sinc, aunque puede optimizarse mediante tablas precalculadas o procesamiento vectorizado.

El parámetro a actúa como control de calidad y rendimiento:

  • a = 2: resultado más rápido, menos preciso.

  • a = 3: equilibrio ideal entre detalle y costo (valor usado en esta implementación).

  • a > 3: mejora marginal de calidad, pero con un aumento notable en el tiempo de cálculo.

En una comparación lado a lado con los métodos anteriores (Bilinear y Bicubic), la interpolación Lanczos muestra:

  • Bordes más definidos sin escalones visibles.

  • Preservación de texturas finas.

  • Menor suavizado artificial en áreas con patrones repetitivos.

Bilineal (izq.) · Bicúbica (centro) · Lanczos (der.)

Bilineal (izq.) · Bicúbica (centro) · Lanczos (der.)

Por ello, este método suele ser la elección preferida en aplicaciones de procesamiento fotográfico profesional, restauración de imágenes o redimensionamiento de dataset de alta calidad.

5. Limitaciones de la interpolación y el camino hacia la superresolución

Las técnicas de interpolación tradicionales —como el vecino más cercano, bilineal, bicúbica o Lanczos— parten de un supuesto fundamental:

La imagen original contiene suficiente información para estimar razonablemente los valores intermedios.

En otras palabras, estos métodos no crean información nueva, solo estiman valores faltantes a partir de los píxeles existentes.

Pérdida de información al escalar

Cuando una imagen se amplía significativamente (por ejemplo, 4× o más), el proceso de interpolación comienza a evidenciar sus límites:

  • Difuminado generalizado: los bordes pierden nitidez progresivamente.

  • Pérdida de texturas finas: patrones como cabello, pasto o piel se suavizan hasta volverse irreconocibles.

  • Artefactos de interpolación: algunos métodos producen halos, aliasing o patrones ondulados (especialmente en bicúbica o Lanczos).

Esto ocurre porque el algoritmo no puede inferir frecuencias espaciales que no existen en la imagen original: una textura de alta frecuencia se pierde si no está codificada en los píxeles.

Por tanto, el límite fundamental de la interpolación clásica está dado por el teorema de muestreo de Nyquist: no se pueden reconstruir detalles más finos que la mitad de la frecuencia máxima presente en los datos originales.

6. De la interpolación al aprendizaje profundo

Ante estas limitaciones surgieron las técnicas de superresolución (SR), cuyo objetivo es reconstruir detalles plausibles en imágenes de baja resolución.
A diferencia de la interpolación, que se basa en operaciones matemáticas locales, la superresolución utiliza redes neuronales profundas para aprender cómo debería lucir una imagen de alta resolución.

Modelos como SRCNN, EDSR o ESRGAN han demostrado que las redes pueden generar resultados con bordes definidos y texturas más naturales, al aprovechar el conocimiento aprendido de millones de imágenes.

En lugar de estimar píxeles faltantes mediante promedios, estas redes reconstruyen patrones visuales: detalles en cabello, piel, texto o superficies que las técnicas clásicas simplemente no pueden recuperar.
En artículos posteriores planeo abordar estas técnicas de superresolución con mayor profundidad, ya que, con la actual explosión de la IA generativa, han surgido numerosos modelos y arquitecturas especialmente interesantes que merecen análisis detallado.

More from this blog