Problema La mayoría de los algoritmos científicos están - Procesamiento de imágenes y análisis, fundamentos del aprendizaje automático, etc. Los investigadores escriben MATLAB, publican artículos y comparten los archivos. MATLAB .m Pero el código de producción basado en estos algoritmos se ejecuta en otro lugar. Si está construyendo una aplicación web que necesita métricas de calidad de imagen, está escribiendo JavaScript. Así que encuentra un papel, lee el algoritmo y lo reimplementa. Puedes escribir pruebas de unidades. Compruebe que las imágenes idénticas devuelvan 1.0, y que las diferentes imágenes devuelvan un valor más bajo. Pero eso sólo captura errores obvios. Los sutiles son el manejo de límites incorrectos, los coeficientes están ligeramente descartados o faltan pasos de normalización. Su código corre y devuelve números plausibles, pero no está haciendo lo que describe el documento. Las bibliotecas JavaScript existentes tienen este problema.Están portando otras puertas de lenguaje de bajo nivel, cada generación desviando más lejos del original.Nadie valida contra la referencia MATLAB porque MATLAB cuesta dinero, y ejecutarlo en CI no es trivial. Yo quería implementaciones fiables de SSIM (Structural Similarity Index) y GMSD (Gradient Magnitude Similarity Deviation). Las opciones de JavaScript eran lentas, inexactas o ambas. Idea: Usar Octave para la verificación Es una alternativa gratuita y de código abierto a MATLAB. archivos, produce las mismas salidas, e instala en cualquier servidor CI. La idea es simple: GNU Octave .m Tome la implementación original de MATLAB del papel Executarlo en Octave con imágenes de prueba Executa tu implementación de JavaScript con las mismas imágenes Compara los resultados Esto no es una prueba de unidad. Es una validación de la verdad fundamental. No está verificando que su código se comporte correctamente según su comprensión. Está verificando que coincide exactamente con la implementación de referencia. Aquí está lo que parece en la práctica: function runMatlabGmsd(img1Path: string, img2Path: string): number { const matlabScript = ` addpath('${matlabPath}'); img1 = imread('${img1Path}'); img2 = imread('${img2Path}'); if size(img1, 3) == 3, img1 = rgb2gray(img1); end if size(img2, 3) == 3, img2 = rgb2gray(img2); end result = GMSD(double(img1), double(img2)); fprintf('%.15f', result); `; const output = execSync(`octave --eval "${matlabScript}"`, { encoding: 'utf-8', }); const match = output.match(/(\d+\.\d+)/); return parseFloat(match[0]); } Llamas a Octave desde Node.js, analizas el resultado del punto flotante y lo comparas con tu salida de JavaScript. Inicio > GMSD GMSD mide la similitud de la imagen utilizando gradientes. Calcula la información de borde para ambas imágenes, las compara píxel por píxel y devuelve una única puntuación. El algoritmo se basa en Es sencillo, bien documentado y no tiene una implementación de JavaScript existente. Xue, W., Zhang, L., Mou, X., & Bovik, A. C. (2013). "División de similitud de magnitud gradual: un índice de calidad de imagen perceptiva altamente eficiente." Referencias de MATLAB Hay 35 líneas: La implementación original function [score, quality_map] = GMSD(Y1, Y2) % Gradient Magnitude Similarity Deviation % Wufeng Xue, Lei Zhang, Xuanqin Mou, and Alan C. Bovik T = 170; Down_step = 2; dx = [1 0 -1; 1 0 -1; 1 0 -1]/3; dy = dx'; aveKernel = fspecial('average', 2); aveY1 = conv2(Y1, aveKernel, 'same'); aveY2 = conv2(Y2, aveKernel, 'same'); Y1 = aveY1(1:Down_step:end, 1:Down_step:end); Y2 = aveY2(1:Down_step:end, 1:Down_step:end); IxY1 = conv2(Y1, dx, 'same'); IyY1 = conv2(Y1, dy, 'same'); gradientMap1 = sqrt(IxY1.^2 + IyY1.^2); IxY2 = conv2(Y2, dx, 'same'); IyY2 = conv2(Y2, dy, 'same'); gradientMap2 = sqrt(IxY2.^2 + IyY2.^2); quality_map = (2*gradientMap1.*gradientMap2 + T) ./ (gradientMap1.^2 + gradientMap2.^2 + T); score = std2(quality_map); : The algorithm Desmontar ambas imágenes por 2x usando un filtro de mediación Calcular magnitudes de gradientes con operadores Prewitt (3x3 detección de borde) Calcular la similitud por píxel 4.Retorna la desviación estándar del mapa de similitud El puerto de JavaScript La idea central: las imágenes que parecen similares tienen bordes similares. Una foto y su versión ligeramente comprimida tienen bordes en los mismos lugares. GMSD captura esto calculando la "magnitud de gradiente" - esencialmente, cuán fuertes son las bordes en cada píxel. Usa el operador Prewitt, un filtro clásico 3x3 que detecta los cambios de intensidad horizontal y vertical. La magnitud combina ambas direcciones: . sqrt(horizontal² + vertical²) En MATLAB, este es un lineal con En JavaScript, pasamos por cada píxel y sus 8 vecinos: conv2 function computeGradientMagnitudes( luma: Float32Array, width: number, height: number ): Float32Array { const grad = new Float32Array(width * height); for (let y = 1; y < height - 1; y++) { for (let x = 1; x < width - 1; x++) { const idx = y * width + x; // Fetch 3x3 neighborhood const tl = luma[(y - 1) * width + (x - 1)]; const tc = luma[(y - 1) * width + x]; const tr = luma[(y - 1) * width + (x + 1)]; const ml = luma[y * width + (x - 1)]; const mr = luma[y * width + (x + 1)]; const bl = luma[(y + 1) * width + (x - 1)]; const bc = luma[(y + 1) * width + x]; const br = luma[(y + 1) * width + (x + 1)]; // Prewitt Gx = [1 0 -1; 1 0 -1; 1 0 -1]/3 const gx = (tl + ml + bl - tr - mr - br) / 3; // Prewitt Gy = [1 1 1; 0 0 0; -1 -1 -1]/3 const gy = (tl + tc + tr - bl - bc - br) / 3; grad[idx] = Math.sqrt(gx * gx + gy * gy); } } return grad; } Una vez que haya magnitudes de gradientes para ambas imágenes, GMSD las compara píxel por píxel y devuelve la desviación estándar de las diferencias. function computeStdDev(values: Float32Array): number { const len = values.length; if (len === 0) return 0; let sum = 0; for (let i = 0; i < len; i++) { sum += values[i]; } const mean = sum / len; let variance = 0; for (let i = 0; i < len; i++) { const diff = values[i] - mean; variance += diff * diff; } variance /= len; return Math.sqrt(variance); } Validación Aquí es donde Octave gana su mantenimiento. ejecutamos el código original MATLAB y nuestra implementación JavaScript en las mismas imágenes y comparamos las salidas. function runMatlabGmsd(img1Path: string, img2Path: string): number { const matlabPath = join(__dirname, '../matlab'); const matlabScript = [ `addpath('${matlabPath}')`, `img1 = imread('${img1Path}')`, `img2 = imread('${img2Path}')`, `if size(img1, 3) == 3, img1 = rgb2gray(img1); end`, `if size(img2, 3) == 3, img2 = rgb2gray(img2); end`, `result = GMSD(double(img1), double(img2))`, `fprintf('%.15f', result)`, ].join('; '); const output = execSync(`octave --eval "${matlabScript}"`, { encoding: 'utf-8', }); const match = output.match(/(\d+\.\d+)/); return parseFloat(match[0]); } Rompiendo esto abajo: addpath - le dice a Octave dónde encontrar GMSD.m (el archivo MATLAB original del papel) imread — carga las imágenes de prueba rgb2gray - se convierte a escala gris si es necesario (GMSD sólo funciona en luminancia) doble() — MATLAB/Octave necesita entrada de punto flotante, no uint8 fprintf('%.15f') — saca 15 puntos decimales, suficiente precisión para capturar errores sutiles El regex extrae el número de la salida de Octave. Octave imprime alguna información de inicio, por lo que solo captamos el resultado de punto flotante. Usando Vitest, pero Jest o cualquier otro marco funciona de la misma manera: import { execSync } from 'node:child_process'; import { describe, expect, it } from 'vitest'; import { gmsd } from './index'; describe('GMSD - MATLAB Comparison', () => { it('should match MATLAB reference', async () => { const tsResult = gmsd(img1.data, img2.data, undefined, width, height, { downsample: 1, c: 170, }); const matlabResult = runMatlabGmsd(img1Path, img2Path); const percentDiff = Math.abs(tsResult - matlabResult) / matlabResult * 100; console.log(`TypeScript: ${tsResult.toFixed(6)}`); console.log(`MATLAB: ${matlabResult.toFixed(6)}`); console.log(`Diff: ${percentDiff.toFixed(2)}%`); // Accept <2% difference (boundary handling varies) expect(percentDiff).toBeLessThan(2); }); }); La clave es establecer la tolerancia correcta. Demasiado estricto (0.0001%) y vas a perseguir a los fantasmas de puntos flotantes. Demasiado suave (10%) y los errores reales se escapan. He aterrizado en el 2% para GMSD después de entender las fuentes de las diferencias (más sobre eso en Resultados). Para CI, Octave instala en segundos en Ubuntu: name: Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Octave run: sudo apt-get update && sudo apt-get install -y octave - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: pnpm install - name: Run tests run: pnpm test Cada empuje ahora valida tu JavaScript contra la referencia MATLAB. Si rompes accidentalmente el manejo de límites o confundes un coeficiente, la prueba fracasará. Resultados Image Pair TypeScript MATLAB Difference 1a vs 1b 0.088934 0.089546 0.68% 2a vs 2b 0.142156 0.143921 1.23% 3a vs 3b 0.067823 0.068412 0.86% 1a vs 1b 0.088934 0.089546 0,68 por ciento 2a vs 2b 0.142156 0.143921 1.23 por ciento 3a vs 3b 0.067823 0.068412 0,86 por ciento La diferencia del 0,68-1,23% proviene del manejo fronterizo en Los de Matlab Como una métrica de calidad perceptual, esta diferencia es aceptable porque las clasificaciones relativas de los pares de imágenes permanecen idénticas. conv2 'same' Escala arriba: SSIM SSIM (Structural Similarity Index) es el estándar de la industria para la evaluación de la calidad de la imagen. Compara la luminosidad, el contraste y la estructura entre dos imágenes y devuelve una puntuación de 0 a 1. El algoritmo proviene de Evaluación de la calidad de la imagen: de la visibilidad del error a la similitud estructural. Es ampliamente citado, bien comprendido y ya tiene implementaciones de JavaScript. La complicación es que no estamos comenzando desde cero. Estamos entrando en un espacio con bibliotecas existentes, por lo que podemos comparar directamente la precisión. Wang, Z., Bovik, A. C., Sheikh, H. R., y Simoncelli, E. P (2004) Transacciones de IEEE en el procesamiento de imágenes El paisaje existente La implementación de JavaScript más popular es Funciona. Devuelve números plausibles. pero nadie lo ha validado contra la referencia MATLAB. Sím.js I ran the comparison. The results were surprising: Library Difference from MATLAB ssim.js 0.05%-0.73% @blazediff/ssim 0.00%-0.03% Sím.js 0.05%-0.73% @Blazediff / Sím 0,00 % 0,03 % Eso es hasta 24 veces más preciso. No porque ssim.js esté mal escrito, es una implementación razonable. Pero las pequeñas decisiones se acumulan: ventanas de Gauss ligeramente diferentes, falta de muestreo automático, manejo de límites diferentes. Cada una agrega un error. Referencias de MATLAB La implementación de referencia de Wang es más compleja que GMSD. La adición clave es la reducción automática de muestreo. Las imágenes grandes se escalan antes de la comparación. Esto coincide con la percepción humana (no notamos diferencias de píxeles únicos en una imagen 4K) y mejora el rendimiento. El núcleo de computación: % Automatic downsampling f = max(1, round(min(M,N)/256)); if(f > 1) lpf = ones(f,f); lpf = lpf/sum(lpf(:)); img1 = imfilter(img1, lpf, 'symmetric', 'same'); img2 = imfilter(img2, lpf, 'symmetric', 'same'); img1 = img1(1:f:end, 1:f:end); img2 = img2(1:f:end, 1:f:end); end C1 = (K(1)*L)^2; C2 = (K(2)*L)^2; window = window/sum(sum(window)); mu1 = filter2(window, img1, 'valid'); mu2 = filter2(window, img2, 'valid'); mu1_sq = mu1.*mu1; mu2_sq = mu2.*mu2; mu1_mu2 = mu1.*mu2; sigma1_sq = filter2(window, img1.*img1, 'valid') - mu1_sq; sigma2_sq = filter2(window, img2.*img2, 'valid') - mu2_sq; sigma12 = filter2(window, img1.*img2, 'valid') - mu1_mu2; ssim_map = ((2*mu1_mu2 + C1).*(2*sigma12 + C2)) ./ ... ((mu1_sq + mu2_sq + C1).*(sigma1_sq + sigma2_sq + C2)); mssim = mean2(ssim_map); The algorithm: Prueba inferior si la imagen es mayor que 256px en cualquier lado Aplique una ventana gaussiana (11x11, σ=1.5) para calcular estadísticas locales Calcula la media (μ), la varianza (σ2) y la covarianza para cada ventana 4 Combinar en la fórmula SSIM con las constantes de estabilidad C1 y C2 Devuelve el promedio de todos los valores SSIM locales El puerto de JavaScript Tres partes necesitaban atención cuidadosa: las ventanas de Gauss, la convolución separable y el filtro de muestreo. Ventanas de Gaudí de MATLAB crea un núcleo 11x11 Gaussian. El equivalente JavaScript: fspecial('gaussian', 11, 1.5) function createGaussianWindow1D(size: number, sigma: number): Float32Array { const window = new Float32Array(size); const center = (size - 1) / 2; const twoSigmaSquared = 2 * sigma * sigma; let sum = 0; for (let i = 0; i < size; i++) { const d = i - center; const value = Math.exp(-(d * d) / twoSigmaSquared); window[i] = value; sum += value; } // Normalize so weights sum to 1 for (let i = 0; i < size; i++) { window[i] /= sum; } return window; } Tenga en cuenta que esto crea una ventana 1D, no una 2D. Esto es intencional porque los filtros Gaussianos son separables, lo que significa que una convolución 2D puede ser dividida en dos pases 1D. El mismo resultado, la mitad de las operaciones. Conversión separada MATLAB's aplica un núcleo 2D. Para una ventana 11x11 en una imagen 1000x1000, eso es 121 millones de multiplicaciones. filter2 function convolveSeparable( input: Float32Array, output: Float32Array, temp: Float32Array, width: number, height: number, kernel1d: Float32Array, kernelSize: number ): void { const pad = Math.floor(kernelSize / 2); // Pass 1: Horizontal convolution (input -> temp) for (let y = 0; y < height; y++) { const rowStart = y * width; for (let x = pad; x < width - pad; x++) { let sum = 0; const srcStart = rowStart + x - pad; for (let k = 0; k < kernelSize; k++) { sum += input[srcStart + k] * kernel1d[k]; } temp[rowStart + x] = sum; } } // Pass 2: Vertical convolution (temp -> output) const outWidth = width - kernelSize + 1; const outHeight = height - kernelSize + 1; let outIdx = 0; for (let y = 0; y < outHeight; y++) { for (let x = 0; x < outWidth; x++) { let sum = 0; const srcX = x + pad; for (let k = 0; k < kernelSize; k++) { sum += temp[(y + k) * width + srcX] * kernel1d[k]; } output[outIdx++] = sum; } } } Descarga automática Esta es la parte que la mayoría de las implementaciones de JavaScript saltan. la referencia de MATLAB desprende imágenes mayores que 256px usando un filtro de caja con padding simétrico: function downsampleImages( img1: Float32Array, img2: Float32Array, width: number, height: number, f: number ): { img1: Float32Array; img2: Float32Array; width: number; height: number } { // Create 1D averaging filter const filter1d = new Float32Array(f); const filterValue = 1 / f; for (let i = 0; i < f; i++) { filter1d[i] = filterValue; } // Apply separable filter with symmetric padding const temp = new Float32Array(width * height); const filtered1 = new Float32Array(width * height); const filtered2 = new Float32Array(width * height); convolveSeparableSymmetric(img1, filtered1, temp, width, height, filter1d, f); convolveSeparableSymmetric(img2, filtered2, temp, width, height, filter1d, f); // Subsample by taking every f-th pixel const newWidth = Math.floor(width / f); const newHeight = Math.floor(height / f); const downsampled1 = new Float32Array(newWidth * newHeight); const downsampled2 = new Float32Array(newWidth * newHeight); for (let y = 0; y < newHeight; y++) { for (let x = 0; x < newWidth; x++) { downsampled1[y * newWidth + x] = filtered1[y * f * width + x * f]; downsampled2[y * newWidth + x] = filtered2[y * f * width + x * f]; } } return { img1: downsampled1, img2: downsampled2, width: newWidth, height: newHeight }; } Saltar este paso no rompe nada obvio.Todavía obtienes puntuaciones SSIM.Pero no coincidirán con la referencia, y el algoritmo se ejecutará más lentamente en imágenes grandes. Validación Same pattern as GMSD. Call Octave, compare results: function runMatlabSsim(img1Path: string, img2Path: string): number { const matlabPath = join(__dirname, '../matlab'); const matlabScript = [ `addpath('${matlabPath}')`, `img1 = imread('${img1Path}')`, `img2 = imread('${img2Path}')`, `if size(img1, 3) == 3, img1 = rgb2gray(img1); end`, `if size(img2, 3) == 3, img2 = rgb2gray(img2); end`, `result = ssim(double(img1), double(img2))`, `fprintf('%.15f', result)`, ].join('; '); const output = execSync(`octave --eval "${matlabScript}"`, { encoding: 'utf-8', }); const match = output.match(/(\d+\.\d+)/); return parseFloat(match[0]); } La tolerancia es más estricta aquí. SSIM es un algoritmo bien definido con coeficientes exactos. Si estamos más de 0,05% de baja, algo está mal: it('should match MATLAB for images 1a vs 1b', async () => { const tsResult = ssim(png1.data, png2.data, undefined, width, height); const matlabResult = runMatlabSsim(img1Path, img2Path); const percentDiff = Math.abs(tsResult - matlabResult) / matlabResult * 100; // Strict: must be within 0.05% expect(percentDiff).toBeLessThan(0.05); }); Resultados Image Pair @blazediff/ssim MATLAB Difference 1a vs 1b 0.968753 0.968755 0.00% 2a vs 2b 0.912847 0.912872 0.00% 3a vs 3b 0.847621 0.847874 0.03% 1a vs 1b 0.968753 0.968755 0,00 por ciento 2a vs 2b 0.912847 0.912872 0.00% 3a vs 3b 0.847621 0.847874 0.03% Comparado con ssim.js en las mismas imágenes: Image Pair ssim.js MATLAB Difference 1a vs 1b 0.969241 0.968755 0.05% 2a vs 2b 0.916523 0.912872 0.40% 3a vs 3b 0.853842 0.847874 0.73% 1a vs 1b 0.969241 0.968755 0,05 por ciento 2a vs 2b 0.916523 0.912872 0.40% 3a vs 3b 0.853842 0.847874 0.73% La diferencia de 0,00-0,03% en mi implementación se debe al arreglo de puntos flotantes. Es inevitable cuando se traduce entre idiomas. Las puntuaciones son funcionalmente idénticas a MATLAB. Las diferencias de ssim.js provienen de elecciones algorítmicas: no hay muestreo automático, aproximación de Gauss ligeramente diferente y manejo de límites diferentes. Pistas comunes Portar MATLAB a JavaScript parece sencillo hasta que no lo es. Estos son los problemas que me costaron tiempo. Array Indexing Las matrices de MATLAB comienzan en 1. las matrices de JavaScript comienzan en 0. Todo el mundo lo sabe. El error rara vez es obvio. No obtendrá un error off-by-one en el primer píxel. obtendrá valores ligeramente equivocados en los límites de la imagen, donde los loop como Traducción a El acceso de los vecinos Se convierte , que alcanza el índice -1 cuando Es el 0. for i = 1:height for (let i = 0; i < height; i++) img(i-1, j) img[(i-1) * width + j] i Corrección: dibujar un ejemplo de 3x3 en papel antes de escribir el ciclo. Columna Mayor vs Rojo Mayor MATLAB almacena matrices columna por columna. JavaScript TypedArrays almacena fila por fila. Una matriz 3x3 en MATLAB: [1 4 7] [2 5 8] → stored as [1, 2, 3, 4, 5, 6, 7, 8, 9] [3 6 9] La misma matriz en JavaScript: [1 4 7] [2 5 8] → stored as [1, 4, 7, 2, 5, 8, 3, 6, 9] [3 6 9] Esto es importante cuando se traduce la indexación. Se convierte En JavaScript, no . img(row, col) img[row * width + col] img[col * height + row] Most image libraries already hand you row-major data, so you're fine. But if you're copying MATLAB matrix operations verbatim, watch out. Boundary Handling de MATLAB , , y Tienen diferentes comportamientos defectuosos: conv2 filter2 imfilter conv2(A, K) — sin padding, la salida se reduce Conv2 (A, K, 'similar') — cero padding, salida del mismo tamaño que la entrada filtro2(K, A, 'valido') — no hay padding, la salida se reduce imfilter(A, K, 'simétrico') — padding de espejo en los bordes Tome esto mal, y sus resultados se diferenciarán en cada píxel cerca del límite. Para una imagen de 1000x1000 con un núcleo de 11x11, eso es ~4% de sus píxeles. La referencia SSIM utiliza con padding for downsampling, then con Deja de lado cualquier detalle, y te preguntarás por qué tus números son 2% de descuento. imfilter 'symmetric' filter2 'valid' Coeficientes de espacio color Convertir RGB a grayscale parece simple. Multiplicar por coeficientes, sumar los canales. Pero ¿qué coeficientes? de MATLAB En el caso de BT.601: rgb2gray Y = 0.298936 * R + 0.587043 * G + 0.114021 * B Some JavaScript libraries use BT.709: Y = 0.2126 * R + 0.7152 * G + 0.0722 * B Otros usan la media simple: Y = (R + G + B) / 3 La diferencia es insignificante para la mayoría de las imágenes. Pero si está validando contra MATLAB, use los coeficientes exactos de BT.601. De lo contrario, perseguirás errores fantasmas que realmente son sólo un mal ajuste de conversión en escala gris. Comportamiento implícito de MATLAB MATLAB hace cosas automáticamente que JavaScript no hará: de MATLAB Convierte uint8 (0-255) a float (0.0-255.0). Si su JavaScript lee los datos PNG como Uint8ClampedArray y se olvida de convertir, sus cálculos de varianza se sobrecargarán. Auto-casting double(img) Las funciones MATLAB tienen valores predeterminados enterrados en su documentación. , de , tamaño de ventana 11, sigma 1.5. Default parameters K = [0.01, 0.03] L = 255 Optimiza tu JavaScript La precisión viene primero.Pero una vez que tu implementación coincide con MATLAB, el rendimiento importa.Los algoritmos científicos procesan millones de píxeles. Estas optimizaciones hicieron que mi implementación SSIM fuera 25-70% más rápida que ssim.js, manteniendo la precisión. Uso de tipografías Los arreglos regulares de JavaScript son flexibles. Pueden tener tipos mixtos, crecer dinámicamente y tener métodos convenientes. // Slow: regular array const pixels = []; for (let i = 0; i < width * height; i++) { pixels.push(image[i] * 0.299); } // Fast: TypedArray const pixels = new Float32Array(width * height); for (let i = 0; i < width * height; i++) { pixels[i] = image[i] * 0.299; } TypedArrays tienen tamaño fijo, tipo fijo y memoria contiguo. El motor JavaScript sabe exactamente cómo optimizarlos. Para el código numérico, esto es una aceleración de 2-5x. Uso para la mayoría de las computadoras. cuando necesita una precisión adicional (acumulando sumas sobre imágenes grandes). para la salida de imagen final. Float32Array Float64Array Uint8ClampedArray Separable Filters Una convolución 2D con un núcleo NxN requiere multiplicaciones N2 por píxel. Pero los filtros gaussianos son separables. Un gaussiano 2D es el producto externo de dos gaussianos 1D. En lugar de un pase 11x11, hace dos pases 11x1: 22 operaciones por píxel. // Slow: 2D convolution (N² operations per pixel) for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let sum = 0; for (let ky = 0; ky < kernelSize; ky++) { for (let kx = 0; kx < kernelSize; kx++) { sum += getPixel(x + kx, y + ky) * kernel2d[ky][kx]; } } output[y * width + x] = sum; } } // Fast: separable convolution (2N operations per pixel) // Pass 1: horizontal for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let sum = 0; for (let k = 0; k < kernelSize; k++) { sum += getPixel(x + k, y) * kernel1d[k]; } temp[y * width + x] = sum; } } // Pass 2: vertical for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let sum = 0; for (let k = 0; k < kernelSize; k++) { sum += temp[(y + k) * width + x] * kernel1d[k]; } output[y * width + x] = sum; } } This works for any separable kernel: Gaussian, box filter, or Sobel. Check if your kernel is separable before implementing 2D convolution. Reanudaciones de Buffy La asignación de la memoria es costosa. La recogida de basura es peor. Asigna una vez, reutiliza en todas partes. // Slow: allocate per operation function computeVariance(img: Float32Array): Float32Array { const squared = new Float32Array(img.length); // allocation const filtered = new Float32Array(img.length); // allocation // ... } // Fast: pre-allocate and reuse class SSIMComputer { private temp: Float32Array; private squared: Float32Array; constructor(maxSize: number) { this.temp = new Float32Array(maxSize); this.squared = new Float32Array(maxSize); } compute(img: Float32Array): number { // reuse this.temp and this.squared } } Para SSIM, asigno todos los buffers por delante: imágenes a escala gris, imágenes cuadradas, salidas filtradas y el mapa SSIM. Cache Computed Values Algunos valores se reutilizan. No los calcules dos veces. // Slow: recompute Gaussian window every call function ssim(img1, img2, width, height) { const window = createGaussianWindow(11, 1.5); // expensive // ... } // Fast: cache by parameters const windowCache = new Map<string, Float32Array>(); function getGaussianWindow(size: number, sigma: number): Float32Array { const key = `${size}_${sigma}`; let window = windowCache.get(key); if (!window) { window = createGaussianWindow(size, sigma); windowCache.set(key, window); } return window; } La ventana gaussiana para SSIM es siempre 11x11 con σ=1.5. Computar se toma microsegundos. Avoid Bounds Checking in Hot Loops Para los píxeles internos donde se sabe que los índices son válidos, esto es trabajo desperdiciado. // Slow: bounds checking on every access for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const left = x > 0 ? img[y * width + x - 1] : 0; const right = x < width - 1 ? img[y * width + x + 1] : 0; // ... } } // Fast: handle borders separately // Interior pixels (no bounds checking needed) for (let y = 1; y < height - 1; y++) { for (let x = 1; x < width - 1; x++) { const left = img[y * width + x - 1]; // always valid const right = img[y * width + x + 1]; // always valid // ... } } // Handle border pixels separately with bounds checking Pero para una imagen de 1000x1000, estás eliminando millones de cheques condicionales. Resultados Benchmarking en las mismas imágenes, la misma máquina: Implementation Time (avg) vs MATLAB accuracy ssim.js 86ms 0.05-0.73% @blazediff/ssim 64ms 0.00-0.03% Sím.js 86ms 0.05-0.73% @Blazediff / Sím 64 ms 0 0 0 0 0 0 0 0 25% más rápido y más preciso. No debido a algoritmos inteligentes - debido a la ingeniería cuidadosa. Tipo Arrays, filtros separables, reutilización de buffer, ventanas en caché. Para el variante (usa imágenes integrales en lugar de convolución), la brecha es más amplia: 70% más rápido que ssim.js en imágenes grandes. El SSIM de Hitchhiker Takeaways Porting scientific algorithms from MATLAB to JavaScript is mechanical once you have the proper process. Los documentos describen los algoritmos. El código MATLAB los implementa. Estos no son lo mismo. casos de borde, parámetros predeterminados, manejo de límites - están en el código, no en el papel. El archivo. Use the reference implementation .m Es gratis, se ejecuta en todas partes y ejecuta el código MATLAB sin modificación. Configurar pruebas de verdad básica temprano. Ejecutarlas en CI. Cuando sus números coinciden con 4 lugares decimales, está terminado. Cuando no lo hacen, lo sabes de inmediato. Validate with Octave Obtenga su implementación coincidiendo con MATLAB antes de optimizar. Una respuesta equivocada rápida sigue siendo equivocada. Una vez que sea exacta, las optimizaciones son sencillas: Tipo Arrays, filtros separables, reutilización del buffer. Accuracy first, then performance A veces no se ajustará exactamente. manejo de límites, precisión de puntos flotantes y simplificaciones intencionales. Eso está bien. Sabe por qué se diferencia y por cuánto. Escriba. Document your differences La brecha entre MATLAB académico y JavaScript de producción es más pequeña de lo que parece. Los algoritmos son los mismos. La matemática es la misma. Necesitas una forma de verificar que lo has hecho bien. Las implementaciones SSIM y GMSD de este artículo están disponibles en [blazediff.dev] (https://blazediff.dev). MIT licenciado, dependencias cero, validado contra MATLAB.