Problematik Die meisten wissenschaftlichen Algorithmen leben in - Bildverarbeitung und -analyse, maschinelles Lernen, etc. Forscher schreiben MATLAB, veröffentlichen Papiere und teilen Die Dateien. Matlab .m Aber der Produktionscode basierend auf diesen Algorithmen läuft woanders.Wenn Sie eine Web-App bauen, die Bildqualitätsmetriken benötigt, schreiben Sie JavaScript.So finden Sie ein Papier, lesen Sie den Algorithmus und implementieren Sie es erneut.Hier ist das Problem: Wie wissen Sie, dass Ihre Implementierung richtig ist? Sie können Einheitstests schreiben. Überprüfen Sie, ob identische Bilder 1.0 zurückgeben, und dass verschiedene Bilder einen niedrigeren Wert zurückgeben. Aber das fängt nur offensichtliche Fehler. Die feinen sind falsche Grenzbehandlung, leicht ab Koeffizienten oder fehlende Normalisierungsschritte. Ihr Code läuft und gibt plausible Zahlen zurück, aber es tut nicht, was das Papier beschreibt. Bestehende JavaScript-Bibliotheken haben dieses Problem. Sie transportieren andere Sprachports auf niedriger Ebene, wobei jede Generation weiter vom Original entfernt wird. Niemand validiert gegen die MATLAB-Referenz, weil MATLAB Geld kostet, und das Ausführen in CI ist nicht trivial. Ich wollte zuverlässige Implementierungen von SSIM (Structural Similarity Index) und GMSD (Gradient Magnitude Similarity Deviation). Die JavaScript-Optionen waren entweder langsam, ungenau oder beides. Idee: Octave für die Verifizierung verwenden ist eine kostenlose, Open-Source-Alternative zu MATLAB. Dateien, produziert die gleichen Ausgänge und installiert auf jedem CI-Server. Die GNU Octave .m Nehmen Sie die ursprüngliche MATLAB-Implementierung aus dem Papier Führen Sie es in Octave mit Testbilder aus Führen Sie Ihre JavaScript-Implementierung mit den gleichen Bildern aus Vergleiche die Ergebnisse Dies ist kein Einheitstest. Es ist Grundwahrheitsvalidierung. Sie überprüfen nicht, ob sich Ihr Code nach Ihrem Verständnis richtig verhält. Sie überprüfen, ob es genau mit der Referenzimplementierung übereinstimmt. Hier ist, wie es in der Praxis aussieht: 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]); } Sie rufen Octave von Node.js an, analysieren das Ergebnis des schwimmenden Punktes und vergleichen es mit Ihrer JavaScript-Ausgabe. Einfacher Start: GMSD GMSD misst Bildähnlichkeit mithilfe von Gradienten. Es berechnet Kanteninformationen für beide Bilder, vergleicht sie pixel-by-pixel und gibt eine einzige Punktzahl zurück. Der Algorithmus basiert auf Es ist einfach, gut dokumentiert und hat keine bestehende JavaScript-Implementierung. 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 MATLAB Referenz Es gibt 35 Linien: Die ursprüngliche Umsetzung 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 Abmessung beider Bilder um 2x mit einem Durchschnittsfilter Berechnen Sie Gradientgradientgrößen mit Prewitt-Operatoren (3x3 Edge Detection) Berechnen der Ähnlichkeit pro Pixel 4. Geben Sie die Standardabweichung der Ähnlichkeitskarte zurück Der JavaScript-Port Die Kernidee: Bilder, die ähnlich aussehen, haben ähnliche Kanten. Ein Foto und seine leicht komprimierte Version haben Kanten an den gleichen Orten. GMSD erfasst dies, indem es die "Gradientgröße" berechnet - im Wesentlichen, wie stark die Kanten bei jedem Pixel sind. Es verwendet den Prewitt-Operator, einen klassischen 3x3-Filter, der horizontale und vertikale Intensitätsänderungen erkennt. . sqrt(horizontal² + vertical²) In MATLAB ist dies ein One-Liner mit In JavaScript schleifen wir durch jeden Pixel und seine 8 Nachbarn: 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; } Sobald Sie Gradientgrößen für beide Bilder haben, vergleicht GMSD sie von Pixel zu Pixel und gibt die Standardabweichung der Unterschiede zurück. 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); } Validierung Wir führen den ursprünglichen MATLAB-Code und unsere JavaScript-Implementierung auf den gleichen Bildern aus und vergleichen die Ausgänge. 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]); } Brechen Sie das nach unten: — tells Octave where to find (the original MATLAB file from the paper) addpath GMSD.m imread — lädt die Testbilder ein rgb2gray – konvertiert bei Bedarf in Grayscale (GMSD funktioniert nur auf Leuchtkraft) double() — MATLAB/Octave benötigt Floating-Point-Eingabe, nicht uint8 fprintf('%.15f') - Ausgabe 15 Dezimalstellen, ausreichend präzise, um subtile Fehler zu fangen Der Regex extrahiert die Zahl aus der Octave-Ausgabe. Octave druckt einige Startinformationen aus, so dass wir nur das Ergebnis des schwimmenden Punktes erfassen. Mit Vitest, aber Jest oder ein anderes Framework funktioniert dasselbe: 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); }); }); Der Schlüssel besteht darin, die richtige Toleranz festzulegen. Zu streng (0.0001%) und du wirst schwimmende Geister jagen. Zu locker (10%) und echte Fehler durchlaufen. Ich landete bei 2% für GMSD, nachdem ich die Quellen der Unterschiede verstanden habe (mehr dazu in den Ergebnissen). Bei CI installiert Octave in Sekunden auf 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 Jeder Push validiert jetzt Ihre JavaScript gegen die MATLAB-Referenz. Wenn Sie versehentlich Grenzmanagement brechen oder einen Koeffizienten stören, scheitert der Test. Ergebnisse 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 gegen 1b 0.088934 0.089546 0,68 Prozent 2a vs 2b 0.142156 0.143921 1.23 Prozent 3a gegen 3b 0.067823 0.068412 0,86 Prozent Der Unterschied von 0,68 bis 1,23% ergibt sich aus der Grenzbehandlung in von MATLAB Als Wahrnehmungsqualitätsmetrik ist dieser Unterschied akzeptabel, da die relative Rangfolge der Bildpaare identisch bleibt. conv2 'same' Skalieren nach oben: SSIM SSIM (Structural Similarity Index) ist der Branchenstandard für die Bildqualitätsbewertung. Es vergleicht Helligkeit, Kontrast und Struktur zwischen zwei Bildern und gibt eine Punktzahl von 0 bis 1 zurück. Der Algorithmus kommt von paper. Image quality assessment: from error visibility to structural similarity. It's widely cited, well understood, and already has JavaScript implementations. The complication is we're not starting from scratch. We're entering a space with existing libraries, so we can directly compare accuracy. Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P (2004) IEEE Transaktionen auf Bildverarbeitung Die bestehende Landschaft Die beliebteste JavaScript-Implementierung ist Es funktioniert. Es gibt plausible Zahlen zurück, aber niemand hat es gegen die MATLAB-Referenz validiert. mit ssim.js Ich habe den Vergleich durchgeführt.Die Ergebnisse waren überraschend: Library Difference from MATLAB ssim.js 0.05%-0.73% @blazediff/ssim 0.00%-0.03% mit ssim.js 0,05 % 0,03 % @Blazediff / Sime 0,00 % bis 0,03 % 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. MATLAB Referenz Wangs Referenzimplementierung ist komplexer als GMSD. Der Schlüssel ist die automatische Absamplung. Große Bilder werden vor dem Vergleich skaliert. Dies entspricht der menschlichen Wahrnehmung (wir bemerken keine einzelnen Pixelunterschiede in einem 4K-Bild) und verbessert die Leistung. Die Kernrechnung: % 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, wenn das Bild auf jeder Seite größer als 256px ist Anwenden eines Gaussischen Fensters (11x11, σ=1.5) zur Berechnung lokaler Statistiken Berechnen Sie Medium (μ), Varianz (σ2) und Covarianz für jedes Fenster 4 Kombinieren in die SSIM-Formel mit Stabilitätskonstanzen C1 und C2 Gibt den Mittelwert aller lokalen SSIM-Werte zurück Der JavaScript-Port Three parts needed careful attention: Gaussian windows, separable convolution, and the downsampling filter. Gaussian Windows Das Matlab erstellt einen 11x11 Gaussian Kernel. Das JavaScript-Äquivalent: 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; } Beachten Sie, dass dies ein 1D-Fenster erzeugt, kein 2D-Fenster.Das ist vorsätzlich, weil Gaussische Filter trennbar sind, was bedeutet, dass eine 2D-Konvolution in zwei 1D-Pässe aufgeteilt werden kann. getrennte Konversion MATLAB's für ein 11x11-Fenster auf einem 1000x1000-Bild, das sind 121 Millionen Multiplikationen. 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; } } } Automatisches Downsampling Das ist der Teil, den die meisten JavaScript-Implementierungen überspringen. MATLABs Referenz herunterlässt Bilder, die größer als 256px sind, mithilfe eines Boxfilters mit symmetrischem 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 }; } Skipping this step doesn't break anything obvious. You still get SSIM scores. But they won't match the reference, and the algorithm will run slower on large images. Validierung 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]); } Die Toleranz ist hier strenger. SSIM ist ein gut definierter Algorithmus mit genauen Koeffizienten. Wenn wir mehr als 0,05% off sind, ist etwas falsch: 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); }); Results 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 gegen 1b 0.968753 0.968755 0.00% 2a gegen 2b 0.912847 0.912872 0,00 Prozent 3a gegen 3b 0.847621 0.847874 0,03 Prozent 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 gegen 1b 0.969241 0.968755 0,05 Prozent 2a vs 2b 0.916523 0.912872 0,40 Prozent 3a vs 3b 0.853842 0.847874 0,73 Prozent The 0.00-0.03% difference in my implementation is due to floating-point rounding. It’s unavoidable when translating between languages. The scores are functionally identical to MATLAB. Die ssim.js Unterschiede kommen aus algorithmischen Entscheidungen: keine Auto-Downsampling, leicht unterschiedliche Gaussische Annäherung und unterschiedliche Grenzbehandlung. Die gemeinsamen Pitfälle Porting MATLAB to JavaScript looks straightforward until it isn't. These are the issues that cost me time. Array Indexing MATLAB-Arrays beginnen bei 1. JavaScript-Arrays beginnen bei 0. Jeder weiß das. Der Fehler ist selten offensichtlich. Sie erhalten auf dem ersten Pixel keinen Off-by-One-Fehler. Sie erhalten leicht falsche Werte an den Bildgrenzen, wo Schleifen wie translate to Der Nachbar Zugang wird , which hits index -1 when ist 0 for i = 1:height for (let i = 0; i < height; i++) img(i-1, j) img[(i-1) * width + j] i Fix: Zeichne ein 3x3-Beispiel auf Papier, bevor du die Schleife schreibst. Column-Major gegen Row-Major MATLAB speichert Matrizen Spalte für Spalte. JavaScript TypedArrays speichert Zeilen für Zeilen. Eine 3x3 Matrix in MATLAB: [1 4 7] [2 5 8] → stored as [1, 2, 3, 4, 5, 6, 7, 8, 9] [3 6 9] Die gleiche Matrix 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 wird In JavaScript, nicht . img(row, col) img[row * width + col] img[col * height + row] Die meisten Bildbibliotheken übermitteln Ihnen bereits Zeilenmajor-Daten, so dass Sie in Ordnung sind. aber wenn Sie MATLAB-Matrixoperationen verbatim kopieren, achten Sie darauf. Grenzbehandlung Das Matlab , der , and Sie haben unterschiedliche Standardverhaltensweisen: conv2 filter2 imfilter conv2(A, K) – kein Padding, Ausgang schrumpft conv2 (A, K, 'gleich') - Null Padding, Ausgabe gleiche Größe wie der Eingang filter2(K, A, 'gültig') – kein Padding, Ausgang schrumpft imfilter(A, K, 'symmetrisch') — Spiegelpflaster an den Kanten Get this wrong, and your results will differ at every pixel near the border. For a 1000x1000 image with an 11x11 kernel, that's ~4% of your pixels. Die SSIM-Referenz verwendet with padding for downsampling, then with mode for the main computation. Miss either detail, and you'll wonder why your numbers are 2% off. imfilter 'symmetric' filter2 'valid' Color Space Coefficients Die Konvertierung von RGB in Grayscale erscheint einfach. Multipliziert mit Koeffizienten, summieren Sie die Kanäle. Das Matlab Verwenden Sie 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 Andere verwenden den einfachen Durchschnitt: Y = (R + G + B) / 3 Der Unterschied ist für die meisten Bilder vernachlässigbar. Aber wenn Sie gegen MATLAB validieren, verwenden Sie die genauen BT.601-Koeffizienten. Ansonsten werden Sie Phantom-Bugs verfolgen, die wirklich nur ein Grayscale-Konvertierungsmismatch sind. Das implizite Verhalten von MATLAB MATLAB does things automatically that JavaScript won't: von MATLAB 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 , der , Fenstergröße 11, Sigma 1.5. Verpassen Sie eine und Ihre Implementierung unterscheidet sich. Default parameters K = [0.01, 0.03] L = 255 Optimieren Sie Ihre JavaScript Genauigkeit kommt zuerst. Aber sobald Ihre Implementierung MATLAB entspricht, ist die Leistung wichtig. Wissenschaftliche Algorithmen verarbeiten Millionen von Pixeln. These optimizations made my SSIM implementation 25-70% faster than ssim.js while maintaining accuracy. Verwenden TypedArrays Regelmäßige JavaScript-Arrays sind flexibel. Sie können gemischte Typen behalten, dynamisch wachsen und bequeme Methoden haben. // 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 haben eine feste Größe, einen festen Typ und einen zusammenhängenden Speicher. Die JavaScript-Engine weiß genau, wie sie optimiert werden sollen. Benutze für die meisten Computer. verwenden wenn Sie zusätzliche Präzision benötigen (Akkumulierung von Summen über große Bilder). für die endgültige Bildproduktion. Float32Array Float64Array Uint8ClampedArray getrennte Filter Eine 2D-Konvolution mit einem NxN-Kernel erfordert N2-Multiplikationen pro Pixel. But Gaussian filters are separable. A 2D Gaussian is the outer product of two 1D Gaussians. Instead of one 11x11 pass, you do two 11x1 passes: 22 operations 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; } } This works for any separable kernel: Gaussian, box filter, or Sobel. Check if your kernel is separable before implementing 2D convolution. Wiederholung von Buffers 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 } } Für SSIM verteile ich alle Puffer voraus: Grayscale-Bilder, quadratische Bilder, gefilterte Ausgänge und die SSIM-Karte. Cache Computed Values Manche Werte werden wiederverwendet. Berechnen Sie sie nicht zweimal. // 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; } The Gaussian window for SSIM is always 11x11 with σ=1.5. Computing it takes microseconds. But when you're processing thousands of images, microseconds add up. Vermeiden Sie Grenzen in Hot Loops zu überprüfen JavaScript arrays check bounds on every access. For interior pixels where you know indices are valid, this is wasted work. // 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 Aber für ein 1000x1000 Bild entfernen Sie Millionen von bedingten Schecks. Results Benchmarked on the same images, same machine: Implementation Time (avg) vs MATLAB accuracy ssim.js 86ms 0.05-0.73% @blazediff/ssim 64ms 0.00-0.03% mit ssim.js 86 ms 0,05 bis 0,73% @Blazediff / Sime 64 ms 0.00-0.03% 25 % schneller und präziser. Nicht wegen intelligenter Algorithmen – wegen sorgfältiger Technik. TypedArrays, trennbare Filter, Pufferwiederverwendung, Cache-Fenster. Für die Variante (verwendet Integralbilder statt Konvolution), die Lücke ist breiter: 70% schneller als ssim.js auf großen Bildern. Hitchhiker ́s SSIM Nehmen Sie Das Portieren wissenschaftlicher Algorithmen von MATLAB in JavaScript ist mechanisch, sobald Sie den richtigen Prozess haben. . Papers describe algorithms. MATLAB code implements them. These aren't the same thing. Edge cases, default parameters, boundary handling - they're in the code, not the paper. Start with the der Datei. Use the reference implementation .m Es ist kostenlos, läuft überall und führt MATLAB-Code ohne Änderung aus. Setzen Sie Grundwahrheitstests frühzeitig ein. Führen Sie sie in CI aus. Wenn Ihre Zahlen mit 4 Dezimalstellen übereinstimmen, sind Sie fertig. Validate with Octave Lassen Sie Ihre Implementierung MATLAB vor der Optimierung übereinstimmen. Eine schnelle falsche Antwort ist immer noch falsch. Sobald Sie genau sind, sind die Optimierungen einfach: TypedArrays, trennbare Filter, Pufferwiederverwendung. Accuracy first, then performance Manchmal werden Sie nicht genau übereinstimmen. Grenzmanagement, Schwimmpunktpräzision und vorsätzliche Vereinfachungen. Das ist in Ordnung. Wissen, warum Sie sich unterscheiden und wie viel. Document your differences Die Lücke zwischen akademischem MATLAB und Produktions-JavaScript ist kleiner, als es aussieht. Die Algorithmen sind die gleichen. Die Mathematik ist die gleiche. The SSIM and GMSD implementations from this article are available at [blazediff.dev](https://blazediff.dev). MIT licensed, zero dependencies, validated against MATLAB.