44. Álgebra lineal aplicada al procesamiento de imágenes

Una imagen digital puede verse como una matriz de intensidades o un tensor de color. Filtros, ajustes y transformaciones geométricas se expresan mediante operaciones con vectores y matrices.

44.1 Introducción

Procesar una imagen significa transformar sus valores para mejorarla, analizarla, comprimirla o producir un efecto visual.

El álgebra lineal proporciona modelos para los píxeles, los canales de color, los vecindarios y los cambios de coordenadas.

Imagen ⟶ arreglo de números ⟶ operaciones matriciales ⟶ imagen transformada

44.2 Imagen en escala de grises como matriz

Una imagen de alto h y ancho w puede representarse mediante una matriz h × w. Cada elemento indica la intensidad de un píxel.

I(y, x) = intensidad del píxel situado en la fila y y la columna x
const imagen = [
  [0, 64, 128],
  [32, 128, 220],
  [80, 180, 255]
];

console.log(imagen.length);    // alto: 3
console.log(imagen[0].length); // ancho: 3
console.log(imagen[1][2]);     // intensidad: 220

En imágenes de 8 bits, 0 suele representar negro y 255 blanco.

44.3 Imágenes RGB como tensores

Un píxel RGB es un vector (r, g, b). Una imagen en color reúne alto, ancho y tres canales, por lo que puede considerarse un tensor h × w × 3.

RepresentaciónForma
Escala de grisesh × w
RGBh × w × 3
RGBAh × w × 4
Lote de n imágenes RGBn × h × w × 3
const pixel = [120, 200, 80];
const [rojo, verde, azul] = pixel;

console.log({ rojo, verde, azul });

44.4 Almacenamiento lineal RGBA

Las API suelen almacenar píxeles consecutivos en un arreglo plano. Para una imagen RGBA, el índice del canal inicial del píxel (x, y) es:

índice = 4(y · ancho + x)
function leerPixel(datos, ancho, x, y) {
  const indice = 4 * (y * ancho + x);
  return Array.from(datos.slice(indice, indice + 4));
}

const datos = new Uint8ClampedArray([
  255, 0, 0, 255,  0, 255, 0, 255
]);
console.log(leerPixel(datos, 2, 1, 0)); // [0, 255, 0, 255]

44.5 Ajuste de brillo

Aumentar el brillo suma una constante b a cada canal y limita el resultado al intervalo permitido:

p' = limitar(p + b, 0, 255)

Si b ≠ 0, la operación es afín y no lineal, porque el píxel cero no se transforma en cero.

const limitarCanal = valor => Math.max(0, Math.min(255, Math.round(valor)));
const ajustarBrillo = (pixel, cambio) =>
  pixel.map(canal => limitarCanal(canal + cambio));

console.log(ajustarBrillo([100, 220, 250], 30)); // [130, 250, 255]

44.6 Ajuste de contraste

El contraste puede modificarse alejando o acercando cada valor a un punto medio m:

p' = m + c(p − m)
function ajustarContraste(pixel, factor, medio = 128) {
  return pixel.map(canal =>
    Math.max(0, Math.min(255, Math.round(medio + factor * (canal - medio))))
  );
}

console.log(ajustarContraste([80, 128, 180], 1.5)); // [56, 128, 206]

Un factor mayor que 1 aumenta el contraste; uno entre 0 y 1 lo reduce.

44.7 Conversión a escala de grises

Una luminancia aproximada se obtiene mediante un producto escalar entre el color y pesos perceptuales:

Y = 0.2126R + 0.7152G + 0.0722B
function luminancia([r, g, b]) {
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

console.log(luminancia([255, 0, 0]));   // 54.213
console.log(luminancia([0, 255, 0]));   // 182.376
console.log(luminancia([0, 0, 255]));   // 18.411

En flujos con gestión precisa de color también debe considerarse si los canales están codificados o en espacio lineal.

44.8 Transformaciones matriciales de color

Una matriz 3 × 3 mezcla los canales RGB:

c' = Mc

Esta técnica permite intercambiar canales, ajustar saturación o cambiar espacios de color.

function transformarColor(M, color) {
  return M.map(fila =>
    fila.reduce((suma, valor, i) => suma + valor * color[i], 0)
  );
}

const intercambiarRojoAzul = [[0, 0, 1], [0, 1, 0], [1, 0, 0]];
console.log(transformarColor(intercambiarRojoAzul, [200, 100, 50]));
// [50, 100, 200]

44.9 Composición alfa

Para colocar un color frontal F con opacidad α sobre un fondo opaco B:

C = αF + (1 − α)B
function mezclar(frente, fondo, alfa) {
  const a = Math.max(0, Math.min(1, alfa));
  return frente.map((canal, i) =>
    Math.round(a * canal + (1 - a) * fondo[i])
  );
}

console.log(mezclar([255, 0, 0], [0, 0, 255], 0.25));
// [64, 0, 191]

La composición entre capas semitransparentes requiere además combinar correctamente sus valores alfa.

44.10 Núcleos y filtros espaciales

Un núcleo es una matriz pequeña que se desplaza sobre la imagen. En cada posición se calcula un producto escalar entre sus pesos y el vecindario.

salida(y, x) = Σi,j núcleo(i, j) · imagen(y + i, x + j)

En la convolución matemática el núcleo se invierte. Muchas bibliotecas llaman “convolución” a la correlación cruzada, que aplica el núcleo sin invertir. Para núcleos simétricos ambas coinciden.

44.11 Tratamiento de bordes

En los límites de la imagen faltan vecinos. Debe elegirse una política:

PolíticaIdea
CerosConsiderar negro fuera de la imagen
ExtensiónRepetir el píxel más cercano del borde
ReflexiónReflejar la imagen en el límite
EnvolturaContinuar desde el borde opuesto
Solo región válidaProducir una salida más pequeña
function indiceExtendido(indice, longitud) {
  return Math.max(0, Math.min(longitud - 1, indice));
}

console.log(indiceExtendido(-1, 5)); // 0
console.log(indiceExtendido(7, 5));  // 4

44.12 Desenfoque

Un desenfoque de caja 3 × 3 reemplaza cada píxel por el promedio de su vecindario:

K = 1/9 [[1, 1, 1], [1, 1, 1], [1, 1, 1]]

La suma de los pesos es 1, por lo que las regiones constantes conservan su brillo.

const nucleoDesenfoque = Array.from({ length: 3 }, () =>
  Array(3).fill(1 / 9)
);
const sumaPesos = nucleoDesenfoque.flat()
  .reduce((suma, valor) => suma + valor, 0);

console.log(sumaPesos); // aproximadamente 1

44.13 Enfoque y realce

Un núcleo de enfoque aumenta la diferencia entre un píxel y sus vecinos:

K = [[0, −1, 0], [−1, 5, −1], [0, −1, 0]]

Su suma también es 1. Los resultados pueden salir del intervalo [0, 255] y deben limitarse.

const nucleoEnfoque = [[0, -1, 0], [-1, 5, -1], [0, -1, 0]];
const vecindarioPlano = Array(9).fill(100);
const salida = nucleoEnfoque.flat().reduce(
  (suma, peso, i) => suma + peso * vecindarioPlano[i],
  0
);

console.log(salida); // 100

44.14 Detección de bordes con Sobel

Los núcleos de Sobel aproximan cambios horizontales y verticales:

Gx = [[−1, 0, 1], [−2, 0, 2], [−1, 0, 1]]
Gy = [[−1, −2, −1], [0, 0, 0], [1, 2, 1]]

magnitud = √(Gx2 + Gy2)
const gx = 30;
const gy = 40;
const magnitudBorde = Math.hypot(gx, gy);
const orientacion = Math.atan2(gy, gx) * 180 / Math.PI;

console.log(magnitudBorde); // 50
console.log(orientacion);   // aproximadamente 53.13°

44.15 Correlación 2D en JavaScript

Esta implementación aplica un núcleo impar sin invertir y extiende los píxeles del borde.

function filtrar(imagen, nucleo) {
  const alto = imagen.length;
  const ancho = imagen[0].length;
  const radioY = Math.floor(nucleo.length / 2);
  const radioX = Math.floor(nucleo[0].length / 2);

  return imagen.map((fila, y) => fila.map((_, x) => {
    let suma = 0;
    for (let ky = 0; ky < nucleo.length; ky++) {
      for (let kx = 0; kx < nucleo[0].length; kx++) {
        const iy = Math.max(0, Math.min(alto - 1, y + ky - radioY));
        const ix = Math.max(0, Math.min(ancho - 1, x + kx - radioX));
        suma += imagen[iy][ix] * nucleo[ky][kx];
      }
    }
    return suma;
  }));
}

const imagen = [[0, 0, 0], [0, 255, 0], [0, 0, 0]];
const promedio = Array.from({ length: 3 }, () => Array(3).fill(1 / 9));
console.log(filtrar(imagen, promedio));

44.16 Transformaciones geométricas de imágenes

Rotar, escalar o deformar una imagen equivale a cambiar las coordenadas de muestreo. Para cada píxel de salida se aplica la transformación inversa y se busca su origen.

porigen = H-1psalida

El mapeo inverso evita huecos que aparecerían al enviar cada píxel original hacia su destino.

function aplicarHomografia(H, [x, y]) {
  const [a, b, c] = H.map(fila => fila[0] * x + fila[1] * y + fila[2]);
  return c === 0 ? null : [a / c, b / c];
}

const inversaTraslacion = [[1, 0, -5], [0, 1, 2], [0, 0, 1]];
console.log(aplicarHomografia(inversaTraslacion, [10, 10])); // [5, 12]

44.17 Interpolación bilineal

Una coordenada de origen rara vez cae exactamente sobre un píxel. La interpolación bilineal combina los cuatro vecinos según sus distancias.

function bilineal(q00, q10, q01, q11, tx, ty) {
  const arriba = q00 * (1 - tx) + q10 * tx;
  const abajo = q01 * (1 - tx) + q11 * tx;
  return arriba * (1 - ty) + abajo * ty;
}

console.log(bilineal(0, 100, 200, 255, 0.5, 0.5)); // 138.75

El vecino más cercano es más rápido pero produce bordes escalonados; la interpolación bilineal genera transiciones más suaves.

44.18 Aproximación de rango bajo, rendimiento y errores

La descomposición en valores singulares permite aproximar una matriz de imagen conservando solo los componentes más importantes. Una aproximación de rango k puede almacenar menos datos y eliminar parte del ruido.

const columna = [1, 2, 3];
const fila = [10, 20, 30, 40];
const imagenRangoUno = columna.map(a => fila.map(b => a * b));

console.log(imagenRangoUno);
// Toda la matriz se reconstruye a partir de dos vectores.

Para rendimiento se usan arreglos tipados, procesamiento por bloques, GPU y filtros separables cuando existen.

Errores frecuentes son intercambiar x e y, olvidar limitar canales, modificar alfa sin intención, mezclar espacios de color, usar una política de borde implícita, confundir convolución con correlación y mapear píxeles hacia adelante dejando huecos.

44.19 Qué debes recordar de este tema

  • Una imagen en grises es una matriz y una imagen RGB puede verse como un tensor.
  • Un píxel de color es un vector de canales.
  • Brillo y contraste requieren limitar valores al intervalo válido.
  • Las matrices 3 × 3 mezclan canales de color.
  • Los filtros espaciales calculan productos entre núcleos y vecindarios.
  • La política de bordes forma parte de la definición del filtro.
  • El mapeo geométrico inverso evita huecos.
  • La interpolación bilineal combina cuatro píxeles vecinos.

44.20 Conclusión

Representar imágenes como matrices convierte filtros, cambios de color y deformaciones en operaciones algebraicas. La misma perspectiva permite optimizar algoritmos y razonar sobre sus resultados.

En el próximo tema estudiaremos cómo la visión por computadora utiliza estas operaciones para extraer información del mundo visual.