Il problema La maggior parte degli algoritmi scientifici vive in - elaborazione e analisi delle immagini, basi dell'apprendimento automatico, ecc. I ricercatori scrivono MATLAB, pubblicano articoli e condividono dei file. di Matlab .m Ma il codice di produzione basato su questi algoritmi funziona altrove. Se stai costruendo un'app web che ha bisogno di metriche di qualità dell'immagine, stai scrivendo JavaScript. Così trovi una carta, leggi l'algoritmo e lo reimplemente. Ecco il problema: come sai che la tua implementazione è corretta? Puoi scrivere test di unità. Verificare che le immagini identiche restituiscano 1.0, e che le immagini diverse restituiscano un valore inferiore. Ma questo cattura solo i bug evidenti. I sottili sono manipolazione del confine sbagliato, un po 'off coefficienti, o mancanza di passi di normalizzazione. Il codice viene eseguito e restituisce numeri plausibili, ma non fa ciò che la carta descrive. Le librerie JavaScript esistenti hanno questo problema. stanno portando altre porte linguistiche a basso livello, ogni generazione che si allontana dall'originale. Nessuno convalida contro il riferimento MATLAB perché MATLAB costa denaro, e eseguirlo in CI non è triviale. Ho cercato implementazioni affidabili di SSIM (Structural Similarity Index) e GMSD (Gradient Magnitude Similarity Deviation).Le opzioni JavaScript erano lente, inesatte o entrambe. Idea: usare Octave per la verifica è un'alternativa gratuita e open source a MATLAB. file, produce le stesse uscite, e installa su qualsiasi server CI. L'idea è semplice: di GNU Octave .m Prendere l'implementazione MATLAB originale dal foglio Run it in Octave with test images Esegui la tua implementazione JavaScript con le stesse immagini Confronta i risultati Questo non è un test unitario. È una convalida della verità di fondo. Non stai controllando che il tuo codice si comporti correttamente secondo la tua comprensione. stai controllando che corrisponda esattamente all'implementazione di riferimento. Ecco cosa sembra in pratica: 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]); } Chiami Octave da Node.js, analizzi il risultato del punto galleggiante e lo confronti con la tua uscita JavaScript. Inizio semplice: GMSD GMSD misura la somiglianza dell'immagine utilizzando i gradienti. Calcola le informazioni di bordo per entrambe le immagini, le confronta pixel per pixel e restituisce un singolo punteggio. L’algoritmo si basa su È semplice, ben documentato e non ha una implementazione JavaScript esistente. Xue, W., Zhang, L., Mou, X., & Bovik, A. C. (2013). "Gradient Magnitude Similarity Deviation: A Highly Efficient Perceptual Image Quality Index." IEEE Transactions on Image Processing Riferimento MATLAB Ci sono 35 linee: L’attuazione originale 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 Dimensionare entrambe le immagini per 2x utilizzando un filtro di media Calcolare le magnitudini dei gradienti con gli operatori Prewitt (3x3 edge detection) Calcolare la somiglianza per pixel 4.Ritorna la deviazione standard della mappa della somiglianza Il port JavaScript L'idea principale: le immagini che sembrano simili hanno bordi simili. Una foto e la sua versione leggermente compressa hanno bordi negli stessi luoghi. GMSD cattura questo calcolando la "magnitudo di gradiente" - essenzialmente, quanto sono forti i bordi a ciascun pixel. Utilizza l'operatore Prewitt, un classico filtro 3x3 che rileva i cambiamenti di intensità orizzontali e verticali. La magnitudo combina entrambe le direzioni: . sqrt(horizontal² + vertical²) In MATLAB, questo è un one-liner con In JavaScript, cerchiamo ciascun pixel e i suoi 8 vicini: 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 volta ottenute le grandezze dei gradienti per entrambe le immagini, GMSD le confronta pixel per pixel e restituisce la deviazione standard delle differenze. 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); } Validazione Questo è dove Octave guadagna il suo mantenimento. Eseguiamo il codice MATLAB originale e la nostra implementazione JavaScript sulle stesse immagini e confrontiamo le uscite. 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]); } Spezzare questo in basso: addpath - dice a Octave dove trovare GMSD.m (il file MATLAB originale dal documento) imread — carica le immagini di prova rgb2gray - converte in grigio se necessario (GMSD funziona solo sulla luminanza) double() – MATLAB/Octave ha bisogno di input di punto galleggiante, non di uint8 fprintf('%.15f') — output 15 punti decimali, sufficiente precisione per catturare bug sottili Il regex estrae il numero dalla uscita di Octave. Octave stampa alcune informazioni di avvio, quindi cogliamo solo il risultato del punto galleggiante. Usando Vitest, ma Jest o qualsiasi altro framework funziona lo stesso: 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 chiave è quella di impostare la corretta tolleranza. Troppo rigoroso (0.0001%) e tu caccerai i fantasmi dei punti galleggianti. Troppo sciolto (10%) e i veri bug scorrono. Ho atterrato al 2% per GMSD dopo aver capito le fonti delle differenze (più su questo in Risultati). Per CI, Octave installa in secondi su 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 Ogni push ora convalida JavaScript contro il riferimento MATLAB. Se si rompe accidentalmente il controllo dei confini o si confonda un coefficiente, il test fallisce. Risultati 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 Il 0,68% 2a vs 2b 0.142156 0.143921 Il 1,23% 3a vs 3b 0.067823 0.068412 Il 0,86% La differenza del 0,68-1,23% deriva dalla gestione del confine in di Matlab La modalità gestisce i bordi dell'immagine in modo diverso dalla mia implementazione. Come metrica di qualità percettiva, questa differenza è accettabile perché le classifiche relative delle coppie di immagini rimangono identiche. conv2 'same' Scalare su: SSIM SSIM (Structural Similarity Index) è lo standard del settore per la valutazione della qualità dell'immagine. Confronta la luminosità, il contrasto e la struttura tra due immagini e restituisce un punteggio da 0 a 1. L’algoritmo viene da Paper. valutazione della qualità dell'immagine: dalla visibilità degli errori alla similitudine strutturale. È ampiamente citato, ben compreso e ha già implementazioni JavaScript. La complicazione è che non stiamo iniziando da zero. Stiamo entrando in uno spazio con le biblioteche esistenti, in modo da poter confrontare direttamente l'accuratezza. Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P (2004) Transazioni IEEE sul trattamento delle immagini Il paesaggio esistente L'implementazione JavaScript più popolare è . It works. It returns plausible numbers. But nobody has validated it against the MATLAB reference. di ssim.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% di ssim.js 0,05 % 0,03 % @blazediff/sima 0 0 0 0 0 0 0 0 That's up to 24x more accurate. Not because ssim.js is badly written — it's a reasonable implementation. But small decisions accumulate: slightly different Gaussian windows, missing auto-downsampling, different boundary handling. Each one adds an error. Riferimento MATLAB L'implementazione di riferimento di Wang è più complessa di quella di GMSD. L'aggiunta chiave è il downsampling automatico. Le grandi immagini vengono ridimensionate prima del confronto. Questo corrisponde alla percezione umana (non si notano differenze di singoli pixel in un'immagine 4K) e migliora le prestazioni. Il nucleo di calcolo: % 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: Downsample se l'immagine è più grande di 256px su qualsiasi lato Applicare una finestra Gaussiana (11x11, σ=1.5) per calcolare le statistiche locali Calcolare la media (μ), la varianza (σ2) e la covarianza per ogni finestra 4 Combinare nella formula SSIM con le costanti di stabilità C1 e C2 Return the mean of all local SSIM values Il port JavaScript Tre parti avevano bisogno di attenzione: finestre Gaussiane, convoluzione separabile, e il filtro di campionamento. Le finestre di Gauss MATLAB's creates an 11x11 Gaussian kernel. The JavaScript equivalent: 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; } Si noti che questo crea una finestra 1D, non una 2D. Questo è intenzionale perché i filtri Gaussiani sono separabili, il che significa che una convoluzione 2D può essere divisa in due passaggi 1D. Lo stesso risultato, metà delle operazioni. Conversione separata di Matlab applicare un kernel 2D. Per una finestra 11x11 su un'immagine 1000x1000, questo è 121 milioni di moltiplicazioni. Convoluzione separabile lo taglia a 22 milioni: 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; } } } Downsampling automatico This is the part most JavaScript implementations skip. MATLAB's reference downsamples images larger than 256px using a box filter with symmetric padding: 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 }; } Saltare questo passaggio non rompe nulla di ovvio. Si ottiene ancora punteggi SSIM. Ma non corrispondono al riferimento, e l'algoritmo verrà eseguito più lentamente su grandi immagini. Validazione Lo stesso modello di GMSD. Chiamare Octave, confrontare i risultati: 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 tolleranza è più stretta qui. SSIM è un algoritmo ben definito con coefficienti esatti. Se siamo più dello 0,05% off, qualcosa non va: 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); }); Risultati 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 Il 0,00% 2a vs 2b 0.912847 0.912872 0.00% 3a vs 3b 0.847621 0.847874 Il 0,3% Compared to ssim.js on the same images: 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 Il 0,05% 2a vs 2b 0.916523 0.912872 Il 0,40% 3a vs 3b 0.853842 0.847874 0.73% La differenza del 0,00-0,03% nella mia implementazione è dovuta all'arrotondamento dei punti galleggianti. È inevitabile quando si traduce tra le lingue. Le differenze di ssim.js provengono da scelte algoritmiche: nessun campionamento automatico, un approccio Gaussiano leggermente diverso e una gestione dei confini diversa. Common Pitfalls Porting MATLAB to JavaScript looks straightforward until it isn't. These are the issues that cost me time. Array Indexing Gli array di MATLAB iniziano a 1. gli array di JavaScript iniziano a 0. Tutti lo sanno. Il bug è raramente evidente. Non si otterrà un errore off-by-one sul primo pixel. Si otterranno valori leggermente sbagliati ai confini dell'immagine, dove i loop come Tradotto da but the neighbor access becomes , che colpisce l'indice -1 quando is 0. for i = 1:height for (let i = 0; i < height; i++) img(i-1, j) img[(i-1) * width + j] i Risoluzione: disegnare un esempio 3x3 su carta prima di scrivere il ciclo. Colonna maggiore vs Row major MATLAB stores matrices column-by-column. JavaScript TypedArrays store row-by-row. A 3x3 matrix in MATLAB: [1 4 7] [2 5 8] → stored as [1, 2, 3, 4, 5, 6, 7, 8, 9] [3 6 9] La stessa matrice in JavaScript: [1 4 7] [2 5 8] → stored as [1, 4, 7, 2, 5, 8, 3, 6, 9] [3 6 9] This matters when you translate indexing. MATLAB's becomes In JavaScript, non . img(row, col) img[row * width + col] img[col * height + row] La maggior parte delle librerie di immagini ti fornisce già i dati di riga maggiore, quindi stai bene.Ma se stai copiando le operazioni di matrice MATLAB verbatim, attenzione. Atteggiamento di frontiera MATLAB's , , e hanno diversi comportamenti di default: conv2 filter2 imfilter — no padding, output shrinks conv2(A, K) conv2 (A, K, 'simile') - zero padding, uscita della stessa dimensione dell'ingresso filtro2(K, A, 'valido') — nessun padding, la produzione si riduce — mirror padding at edges imfilter(A, K, 'symmetric') Fai questo sbagliato, e i tuoi risultati differiranno a ogni pixel vicino al limite. Per un'immagine 1000x1000 con un kernel 11x11, questo è ~4% dei tuoi pixel. Il riferimento SSIM utilizza con Padding per il downsampling, quindi con Modalità per il calcolo principale. Manca qualsiasi dettaglio, e vi chiederete perché i vostri numeri sono il 2% di sconto. imfilter 'symmetric' filter2 'valid' Coefficienti spaziali di colore Convertire RGB in grigio sembra semplice. Multiplicare per coefficienti, sommare i canali. Ma quali coefficienti? di Matlab utilizza il 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 Others use the simple average: Y = (R + G + B) / 3 The difference is negligible for most images. But if you're validating against MATLAB, use the exact BT.601 coefficients. Otherwise, you'll chase phantom bugs that are really just a grayscale conversion mismatch. MATLAB’s Implicit Behaviours MATLAB does things automatically that JavaScript won't: : MATLAB's converts uint8 (0-255) to float (0.0-255.0). If your JavaScript reads PNG data as Uint8ClampedArray and you forget to convert, your variance calculations will overflow. Auto-casting double(img) : MATLAB functions have default values buried in their documentation. SSIM uses di , dimensione finestra 11, sigma 1.5. Manca uno, e la tua implementazione divergere. Default parameters K = [0.01, 0.03] L = 255 Optimizing Your JavaScript La precisione viene prima.Ma una volta che la tua implementazione corrisponde a MATLAB, la performance conta. Gli algoritmi scientifici elaborano milioni di pixel. These optimizations made my SSIM implementation 25-70% faster than ssim.js while maintaining accuracy. Usare i tipici Gli array JavaScript regolari sono flessibili. Possono contenere tipi misti, crescere in modo dinamico e avere metodi convenienti. // 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 have fixed size, fixed type, and contiguous memory. The JavaScript engine knows precisely how to optimize them. For numerical code, this is a 2-5x speedup. usare per la maggior parte dei computer. usare when you need extra precision (accumulating sums over large images). Use per l’immagine finale. Float32Array Float64Array Uint8ClampedArray Separable Filters Una convoluzione 2D con un kernel NxN richiede moltiplicazioni N2 per pixel. Ma i filtri gaussiani sono separabili. Un gaussiano 2D è il prodotto esterno di due gaussiani 1D. Invece di un passaggio 11x11, fai due passaggi 11x1: 22 operazioni per pixel. // 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; } } Questo funziona per qualsiasi kernel separabile: Gaussian, filtro di casella o Sobel. Controlla se il tuo kernel è separabile prima di implementare la convoluzione 2D. Nuovi buffoni Memory allocation is expensive. Garbage collection is worse. Allocate once, reuse everywhere. // 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 } } Per SSIM, assegnavo tutti i buffer in anticipo: immagini a scala grigia, immagini quadrate, uscite filtrate e la mappa SSIM. Cache Computed Values Alcuni valori vengono riutilizzati. Non calcolarli due volte. // 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 finestra Gaussiana per SSIM è sempre 11x11 con σ=1.5. Il calcolo richiede microsecondi. ma quando si elaborano migliaia di immagini, i microsecondi si aggregano. Avoid Bounds Checking in Hot Loops Per i pixel interni in cui si sa che gli indici sono validi, questo è un lavoro sprecato. // 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 Ma per un'immagine da 1000x1000, stai rimuovendo milioni di controlli condizionali. Risultati Benchmarked sulle stesse immagini, la stessa macchina: Implementation Time (avg) vs MATLAB accuracy ssim.js 86ms 0.05-0.73% @blazediff/ssim 64ms 0.00-0.03% di ssim.js di 86 ms 0.05-0.73% @blazediff/sima 64Ms 0.00-0.03% 25% faster and more accurate. Not because of clever algorithms — because of careful engineering. TypedArrays, separable filters, buffer reuse, cached windows. Per il variant (uses integral images instead of convolution), the gap is wider: 70% faster than ssim.js on large images. Hitchhiker's SSIM di Takeaways Portare gli algoritmi scientifici da MATLAB a JavaScript è meccanico una volta che si dispone del processo corretto. I documenti descrivono gli algoritmi. il codice MATLAB li implementa. Questi non sono la stessa cosa. casi di margine, parametri predefiniti, gestione dei confini - sono nel codice, non nella carta. file. Use the reference implementation .m È gratuito, funziona ovunque e esegue il codice MATLAB senza modifiche. Impostare test di verità di fondo in anticipo. Eseguire in CI. Quando i numeri corrispondono a 4 punti decimali, sei finito. Quando non lo fanno, lo sai immediatamente. Validate with Octave Ottieni la tua implementazione corrispondente a MATLAB prima di ottimizzare. Una risposta sbagliata rapida è ancora sbagliata. Una volta che sei accurato, le ottimizzazioni sono semplici: TypedArrays, filtri separabili, riutilizzo del buffer. Accuracy first, then performance A volte non si corrisponde esattamente. maneggevolezza dei confini, precisione dei punti galleggianti e semplificazioni intenzionali. Questo è ok. Sapere perché si differenziano e per quanto. Scrivi. Document your differences The gap between academic MATLAB and production JavaScript is smaller than it looks. The algorithms are the same. The math is the same. You need a way to verify you got it right. The SSIM and GMSD implementations from this article are available at [blazediff.dev](https://blazediff.dev). MIT licensed, zero dependencies, validated against MATLAB.