Problemet er De fleste videnskabelige algoritmer lever - billedbehandling og analyse, machine learning fundamentals osv. Forskere skriver MATLAB, udgiver papirer og deler af filer. af Matlab .m Men produktionskoden baseret på disse algoritmer kører andre steder. Hvis du bygger en webapp, der har brug for billedkvalitetsmetoder, skriver du JavaScript. Så du finder et papir, læser algoritmen og reimplementerer den. Her er problemet: hvordan ved du, at din implementering er korrekt? Du kan skrive enhedstest. Kontroller, at identiske billeder returnerer 1.0, og at forskellige billeder returnerer en lavere værdi. Men det fanger kun åbenlyse fejl. De subtile er forkerte grænsehåndtering, lidt off koefficienter, eller mangler normalisering trin. Din kode kører og returnerer plausible tal, men det gør ikke, hvad papiret beskriver. Eksisterende JavaScript-biblioteker har dette problem.De transporterer andre sprogporte på lavt niveau, og hver generation flytter længere væk fra originalen.Ingen validerer mod MATLAB-referencen, fordi MATLAB koster penge, og det er ikke trivielt at køre det i CI. Jeg ønskede pålidelige implementeringer af SSIM (Structural Similarity Index) og GMSD (Gradient Magnitude Similarity Deviation). JavaScript-indstillingerne var enten langsomme, unøjagtige eller begge dele. Idé: Brug Octave til verifikation er et gratis, open source alternativ til MATLAB. Det kører det samme filer, producerer de samme outputs og installerer på enhver CI-server. af GNU Octave .m Tag den oprindelige MATLAB-implementering fra papiret Kør det i Octave med testbilleder Kør din JavaScript-implementering med de samme billeder Sammenlign resultaterne Dette er ikke enhedstest. Det er grundlæggende sandhedsvalidering. Du kontrollerer ikke, at din kode opfører sig korrekt i henhold til din forståelse. Du kontrollerer, at den matcher referenceimplementationen nøjagtigt. Her er, hvad det ser ud i praksis: 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]); } Du ringer til Octave fra Node.js, analyserer flydende punktresultatet og sammenligner det med din JavaScript-udgang. Startet enkelt: GMSD GMSD måler billedlignende ved hjælp af gradienter. Det beregner kantinformation for begge billeder, sammenligner dem pixel-for-pixel, og returnerer en enkelt score. Algoritmen er baseret på Den er enkel, veldokumenteret og har ingen eksisterende JavaScript-implementering. Xue, W., Zhang, L., Mou, X., & Bovik, A. C. (2013). "Gradient Magnitude Similarity Deviation: A Highly Efficient Perceptual Image Quality Index." Matlab referencer Der er 35 linjer: Den oprindelige gennemførelse 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 Sample begge billeder ned med 2x ved hjælp af et gennemsnit filter Beregn gradientstørrelser med Prewitt-operatorer (3x3 edge detection) Beregne lighed per pixel 4 Returner standardafvigelsen for lighedskortet Om JavaScript-port Den grundlæggende idé: billeder, der ser ens ud, har lignende kanter. Et billede og dets lidt komprimerede version har kanter på de samme steder. GMSD indfanger dette ved at beregne "gradientstørrelse" - i det væsentlige, hvor stærke kanterne er på hver pixel. Den bruger Prewitt-operatoren, et klassisk 3x3-filter, der registrerer vandrette og lodrette intensitetsændringer. Størrelsen kombinerer begge retninger: . sqrt(horizontal² + vertical²) I MATLAB er dette en en-liner med I JavaScript løber vi gennem hver pixel og dens 8 naboer: 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; } Når du har gradientstørrelser for begge billeder, sammenligner GMSD dem pixel-for-pixel og returnerer standardafvigelsen af forskellene. 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); } Validering Vi kører den oprindelige MATLAB-kode og vores JavaScript-implementering på de samme billeder og sammenligner outputen. 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]); } Bryd det ned: addpath – fortæller Octave, hvor man kan finde GMSD.m (den oprindelige MATLAB-fil fra papiret) imread — indlæser testbillederne rgb2gray - omdanner til gråskala, hvis det er nødvendigt (GMSD fungerer kun på lysstyrke) double() – MATLAB/Octave kræver flydende pointindtastning, ikke uint8 fprintf('%.15f') - udgang 15 decimaler, præcision nok til at fange subtile fejl Octave udskriver nogle opstartsoplysninger, så vi indfanger kun resultatet af flydende punkt. Brug Vitest, men Jest eller enhver anden ramme fungerer på samme måde: 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); }); }); Nøglen er at indstille den rigtige tolerance. For streng (0.0001%) og du vil jage flydende spøgelser. For løs (10%) og rigtige bugs glider igennem. Jeg landede på 2% for GMSD efter at have forstået kilderne til forskellene (mere om det i Resultater). For CI installerer Octave på få sekunder på 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 Hver push validerer nu dit JavaScript mod MATLAB-referencen. Hvis du ved et uheld bryder grænsehåndtering eller forstyrrer en koefficient, fejler testen. Resultater 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 mod 1b 0.088934 0.089546 0,68 procent 2a mod 2b 0.142156 0.143921 1,23 procent 3a mod 3b 0.067823 0.068412 0,86 % af Den 0,68-1,23% forskel kommer fra grænsehåndtering i . MATLAB's mode handles image edges differently than my implementation. As a perceptual quality metric, this difference is acceptable because the relative rankings of image pairs remain identical. conv2 'same' Skalering op: SSIM SSIM (Structural Similarity Index) er industristandard for vurdering af billedkvalitet. Den sammenligner lysstyrke, kontrast og struktur mellem to billeder og returnerer en score fra 0 til 1. Algoritmen kommer fra papir. billedkvalitetsvurdering: fra fejlvisibilitet til strukturel lighed. Det er bredt citeret, godt forstået og allerede har JavaScript-implementeringer. Komplikationen er, at vi ikke starter fra bunden. Vi går ind i et rum med eksisterende biblioteker, så vi direkte kan sammenligne nøjagtigheden. Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P (2004) IEEE Transaktioner om billedbehandling Det eksisterende landskab Den mest populære JavaScript implementering er Det virker. Det returnerer plausible tal. Men ingen har valideret det mod MATLAB-referencen. af ssim.js Jeg lavede sammenligningen, og resultaterne var overraskende: Library Difference from MATLAB ssim.js 0.05%-0.73% @blazediff/ssim 0.00%-0.03% ssim.js 0,05 % 0,03 % @blazediff / Sæson 0,00 % til 0,03 % Det er op til 24x mere præcist. Ikke fordi ssim.js er dårligt skrevet – det er en rimelig implementering. Men små beslutninger akkumuleres: lidt forskellige Gaussian-vinduer, manglende auto-downsampling, forskellig grænsehåndtering. Matlab referencer Wang's reference implementering er mere kompleks end GMSD. Nøgle tilføjelsen er automatisk nedsamling. Store billeder skaleres ned før sammenligning. Dette matcher menneskelig opfattelse (vi bemærker ikke single-pixel forskelle i et 4K billede) og forbedrer ydeevnen. Den grundlæggende beregning: % 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, hvis billedet er større end 256px på nogen side Anvend et gaussisk vindue (11x11, σ=1.5) til at beregne lokal statistik Beregn gennemsnit (μ), varians (σ2) og covariance for hvert vindue 4 Kombiner i SSIM-formlen med stabilitetskonstanter C1 og C2 Returnerer gennemsnittet af alle lokale SSIM-værdier The JavaScript Port Three parts needed careful attention: Gaussian windows, separable convolution, and the downsampling filter. Gaussiske vinduer MATLAB's opretter en 11x11 Gaussian kernel. Den JavaScript-ækvivalent: 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; } Bemærk, at dette skaber et 1D-vindue, ikke et 2D-vindue.Det er hensigtsmæssigt, fordi Gaussian-filtre er adskillelige, hvilket betyder, at en 2D-konvolution kan opdeles i to 1D-pas. Separeret konversion MATLAB's anvendes en 2D-kernel. For et 11x11-vindue på et 1000x1000-billede er det 121 millioner multiplikationer. Separable convolution skærer det til 22 millioner: 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; } } } Automatisk nedtrapning MATLABs reference nedsamler billeder større end 256px ved hjælp af et feltfilter med symmetrisk 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 }; } Hvis du hopper over dette trin, bryder du ikke noget indlysende. Du får stadig SSIM-resultater. Men de matcher ikke referencen, og algoritmen kører langsommere på store billeder. Validering Samme mønster som GMSD. Ring Octave, sammenlign resultaterne: 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]); } Tolerancen er strammere her. SSIM er en veldefineret algoritme med nøjagtige koefficienter. Hvis vi er mere end 0,05% off, er der noget galt: 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); }); Resultater 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 mod 1b 0.968753 0.968755 0,00 % af 2a mod 2b 0.912847 0.912872 0,00 % af 3a mod 3b 0.847621 0.847874 0,03 % af 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 0,05 % af 2a vs 2b 0.916523 0.912872 0.40% 3a mod 3b 0.853842 0.847874 0,73 % af Forskellen på 0,00-0,03% i min implementering skyldes afrunding af flydende punkter. Det er uundgåeligt, når man oversætter mellem sprog. De ssim.js forskelle kommer fra algoritmiske valg: ingen auto-downsampling, lidt anderledes Gaussian approximation, og forskellige grænse håndtering. Almindelige pitfalls Porting MATLAB til JavaScript ser simpel ud, indtil det ikke er. Indeksering af array MATLAB arrays start at 1. JavaScript arrays start at 0. Everyone knows this. Everyone still gets bitten by it. Fejlen er sjældent indlysende. Du får ikke en off-by-one-fejl på den første pixel. Du får lidt forkerte værdier ved billedgrænser, hvor løkker som Oversat til Naboerne har adgang Bliver , som rammer indeks -1 når Det er 0. for i = 1:height for (let i = 0; i < height; i++) img(i-1, j) img[(i-1) * width + j] i Fix: Tegn et 3x3-eksempel på papir, før du skriver loopen. Column-Major vs Row-Major MATLAB stores matrices column-by-column. JavaScript TypedArrays store row-by-row. En 3x3 matrix i MATLAB: [1 4 7] [2 5 8] → stored as [1, 2, 3, 4, 5, 6, 7, 8, 9] [3 6 9] Samme matrix i 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 Bliver I JavaScript er det ikke . 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. Grænsehåndtering af Matlab , der og har forskellige standard adfærd: conv2 filter2 imfilter conv2 (A, K) – ingen padding, udgang krymper conv2 (A, K, 'samme') - nul padding, output samme størrelse som input filter2 (K, A, 'gyldig') — ingen padding, udgang krymper imfilter(A, K, 'symmetrisk') — spejl padding på kanter Gør dette forkert, og dine resultater vil afvige ved hver pixel nær grænsen. For et 1000x1000 billede med en 11x11 kerne, det er ~4% af dine pixels. SSIM-referencen bruger with padding for downsampling, then med Gå glip af hver detalje, og du vil undre dig over, hvorfor dine tal er 2% rabat. imfilter 'symmetric' filter2 'valid' Farve rum koefficienter Converting RGB to grayscale seems simple. Multiply by coefficients, sum the channels. But which coefficients? af Matlab uses BT.601: rgb2gray Y = 0.298936 * R + 0.587043 * G + 0.114021 * B Nogle JavaScript-biblioteker bruger BT.709: Y = 0.2126 * R + 0.7152 * G + 0.0722 * B Others use the simple average: Y = (R + G + B) / 3 Forskellen er ubetydelig for de fleste billeder. Men hvis du validerer mod MATLAB, skal du bruge de nøjagtige BT.601-koefficienter. Ellers vil du jage phantom bugs, der virkelig er bare en gråkonverteringsmismatch. Implicit adfærd i MATLAB MATLAB does things automatically that JavaScript won't: : MATLAB's konverterer uint8 (0-255) til float (0.0-255.0). Hvis dit JavaScript læser PNG-data som Uint8ClampedArray, og du glemmer at konvertere, vil dine variansberegninger oversvømme. Auto-casting double(img) MATLAB-funktioner har standardværdier begravet i deres dokumentation. , der , window size 11, sigma 1.5. Miss one, and your implementation diverges. Default parameters K = [0.01, 0.03] L = 255 Optimer dit JavaScript Nøjagtighed kommer først. Men når din implementering matcher MATLAB, er ydeevnen vigtig. Videnskabelige algoritmer behandler millioner af pixels. These optimizations made my SSIM implementation 25-70% faster than ssim.js while maintaining accuracy. Brug af typografier Regular JavaScript arrays are flexible. They can hold mixed types, grow dynamically, and have convenient methods. That flexibility costs performance. // 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 har fast størrelse, fast type og sammenhængende hukommelse. JavaScript-motoren ved præcis, hvordan man optimerer dem. Brug af for most computations. Use når du har brug for ekstra nøjagtighed (akkumulering af beløb over store billeder). til den endelige billedudgang. Float32Array Float64Array Uint8ClampedArray Separable Filters En 2D-konvolution med en NxN-kernel kræver N2-multiplikationer pr. pixel. Men Gaussian filtre er adskillelige. En 2D Gaussian er det ydre produkt af to 1D Gaussians. I stedet for en 11x11 pass, du gør to 11x1 passes: 22 operationer 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. Tilbage til 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 } } For SSIM tildeler jeg alle buffere på forhånd: gråskalaer, firkantede billeder, filtrerede outputs og SSIM-kortet. Cache beregnede værdier Nogle værdier genbruges. Tæl dem ikke to gange. // 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. Undgå at tjekke grænser i hot loops 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 This is uglier code. But for a 1000x1000 image, you're removing millions of conditional checks. Resultater 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% af ssim.js 86 stk. 0,05 til 0,7 % @blazediff / Sæson 64ms 0.00-0.03% 25% hurtigere og mere præcis. Ikke på grund af smarte algoritmer - på grund af omhyggelig teknik. TypedArrays, adskillelige filtre, buffer genbrug, cachede vinduer. For the varianten (bruger integrerede billeder i stedet for konvolution), gapet er bredere: 70% hurtigere end ssim.js på store billeder. Hitchhiker’s SSIM Tagetøj Porting videnskabelige algoritmer fra MATLAB til JavaScript er mekanisk, når du har den rigtige proces. Papirer beskriver algoritmer. MATLAB-kode implementerer dem. Disse er ikke det samme. Edge-sager, standardparametre, grænsehåndtering - de er i koden, ikke på papiret. af filen. Use the reference implementation .m Det er gratis, kører overalt, og kører MATLAB-kode uden ændringer. Indstil grundlæggende sandhedstests tidligt. Kør dem i CI. Når dine tal matcher 4 decimaler, er du færdig. Validate with Octave Få din implementering matcher MATLAB før optimering. Et hurtigt forkert svar er stadig forkert. Når du er nøjagtig, optimeringerne er enkle: TypedArrays, adskillelige filtre, buffer genbrug. Accuracy first, then performance Nogle gange matcher du ikke nøjagtigt. Grænsehåndtering, flydende punktpræcision og forsætlige forenklinger. Det er okay. vide, hvorfor du adskiller dig og i hvor høj grad. Skriv det ned. 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.