problemem Większość algorytmów naukowych żyje w - przetwarzanie i analiza obrazu, podstawy uczenia maszynowego itp. Naukowcy piszą MATLAB, publikują artykuły i dzielą się nimi i plików. MATLAB .m Ale kod produkcji oparty na tych algorytmach działa gdzie indziej. Jeśli budujesz aplikację internetową, która potrzebuje wskaźników jakości obrazu, piszesz JavaScript. Więc znajdziesz papier, przeczytasz algorytm i ponownie go wdrożysz. Możesz napisać testy jednostkowe. Sprawdź, czy identyczne obrazy zwracają 1.0, a różne obrazy zwracają niższą wartość. Ale to tylko łapie oczywiste błędy. Subtelne błędy są nieprawidłową obsługą granic, lekko wyłączone współczynniki lub brakuje kroków normalizacji. Twój kod działa i zwraca prawdopodobne liczby, ale nie robi to, co opisuje dokument. Istniejące biblioteki JavaScript mają ten problem. portują inne porty językowe niskiego poziomu, każde pokolenie waha się dalej od oryginału.Nikt nie zatwierdza odniesienia MATLAB, ponieważ MATLAB kosztuje pieniądze, a uruchamianie go w CI nie jest trivialne. Chciałem niezawodnych wdrożeń SSIM (Structural Similarity Index) i GMSD (Gradient Magnitude Similarity Deviation). Opcje JavaScript były albo powolne, niedokładne, albo obie. Idea: Użyj Octave do weryfikacji Jest to darmowa, otwarta alternatywa dla MATLAB. pliki, produkuje te same wyjścia i instaluje na dowolnym serwerze CI. GNU Octave .m Pobierz oryginalną implementację MATLAB z papieru Uruchom go w Octave z obrazami testowymi Uruchom wdrożenie JavaScript z tymi samymi obrazami Porównaj wyniki To nie jest test jednostkowy. Jest to walidacja prawdy podstawowej. Nie sprawdzasz, czy twój kod zachowuje się poprawnie zgodnie z twoim zrozumieniem. Sprawdzasz, czy dokładnie pasuje do wdrożenia odniesienia. 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]); } You call Octave from Node.js, parse the floating-point result, and compare it to your JavaScript output. Not compromising ambiguity - taking 15 decimal places. Rozpoczęcie proste: GMSD GMSD measures image similarity using gradients. It computes edge information for both images, compares them pixel-by-pixel, and returns a single score. Lower means more similar. Zero means identical. Algorytm opiera się na Jest prosty, dobrze udokumentowany i nie ma istniejącej implementacji JavaScript. Xue, W., Zhang, L., Mou, X., & Bovik, A. C. (2013). „Odchylenie podobieństwa wielkości stopniowej: wysoce wydajny wskaźnik jakości obrazu percepcyjnego”. Referencje MATLAB Łącznie 35 linii: Oryginalna implementacja 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 Downsample obydwu obrazów 2x za pomocą filtra przeciętnego Obliczanie wielkości gradientów za pomocą operatorów Prewitt (3x3 detekcja krawędzi) Obliczanie podobieństwa na piksel 4.Wróć odchylenie standardowe mapy podobieństwa Porty JavaScript Podstawowa idea: obrazy, które wyglądają podobnie, mają podobne krawędzie. zdjęcie i jego lekko skompresowana wersja mają krawędzie w tych samych miejscach. GMSD rejestruje to, obliczając "gradientną wielkość" - zasadniczo, jak silne są krawędzie na każdym pikselu. Wykorzystuje operator Prewitt, klasyczny filtr 3x3, który wykrywa zmiany intensywności poziomej i pionowej. . sqrt(horizontal² + vertical²) In MATLAB, this is a one-liner with W JavaScript przewijamy każdy piksel i jego 8 sąsiadów: 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; } Po uzyskaniu wielkości gradientów dla obu obrazów, GMSD porównuje je piksel po pikselu i zwraca standardowe odchylenie różnic. Wysoka odchylenie oznacza, że krawędzie znajdują się w różnych miejscach. 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); } Validation Wykonujemy oryginalny kod MATLAB i naszą implementację JavaScript na tych samych obrazach i porównujemy wyniki. 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]); } Złamać to w dół: addpath — mówi Octave, gdzie znaleźć GMSD.m (oryginalny plik MATLAB z papieru) imread — ładuje obrazy testowe — converts to grayscale if needed (GMSD works on luminance only) rgb2gray double() — MATLAB/Octave potrzebuje wejścia pływającego punktu, a nie uint8 fprintf('%.15f') — wyjście 15 miejsc dziesiętnych, wystarczająca precyzja, aby złapać subtelne błędy Octave drukuje niektóre informacje o uruchomieniu, więc gromadzimy tylko wynik pływającego punktu. Używając Vitest, ale Jest lub jakakolwiek inna rama działa tak samo: 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); }); }); Kluczem jest ustawienie właściwej tolerancji. Zbyt rygorystyczne (0.0001%) i będziesz ścigać duchów pływających punktów. Zbyt luźne (10%) i prawdziwe błędy przechodzą. Po zrozumieniu źródeł różnic wylądowałem na 2% dla GMSD (więcej o tym w wynikach). W przypadku CI, Octave instaluje w sekundy na 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 Każde naciśnięcie potwierdza teraz twój JavaScript w stosunku do odniesienia MATLAB. Jeśli przypadkowo złamiesz granicę obsługi lub zepsułeś współczynnik, test zawiodł. Wyniki 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 proc. 2a vs 2b 0.142156 0.143921 1,23 proc. 3a vs 3b 0.067823 0.068412 0,86 proc. Różnica 0,68-1,23% wynika z obsługi granicznej w z MATLAB Jako metryka jakości percepcyjnej różnica ta jest akceptowalna, ponieważ względne rankingi par obrazu pozostają identyczne. conv2 'same' Rozszerzenie: SSIM SSIM (Structural Similarity Index) jest standardem branżowym do oceny jakości obrazu. porównuje jasność, kontrast i strukturę między dwoma obrazami i zwraca ocenę od 0 do 1. The algorithm comes from Ocena jakości obrazu: od widoczności błędów po podobieństwo strukturalne. Jest szeroko cytowany, dobrze zrozumiany i ma już wdrożenia JavaScript. Powikłanie polega na tym, że nie zaczynamy od zera. Wchodzimy w przestrzeń z istniejącymi bibliotekami, dzięki czemu możemy bezpośrednio porównać dokładność. Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P (2004) IEEE Transactions on Image Processing Istniejący krajobraz Najpopularniejszym wdrożeniem JavaScript jest Działa. zwraca liczby prawdopodobne. ale nikt nie zweryfikował go w stosunku do odniesienia MATLAB. Szybki.js Udało mi się przeprowadzić porównanie, a wyniki były zaskakujące: Library Difference from MATLAB ssim.js 0.05%-0.73% @blazediff/ssim 0.00%-0.03% Szybki.js 0,05 % 0,03 % @blazediff / Szym 0,00 % 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. The MATLAB Reference Wdrożenie odniesienia Wang jest bardziej złożone niż GMSD. Kluczowym dodatkiem jest automatyczne pobieranie próbek. Duże obrazy są skalowane przed porównaniem. To pasuje do ludzkiej percepcji (nie zauważamy różnic pojedynczych pikseli w obrazie 4K) i poprawia wydajność. Podstawowe systemy obliczeniowe: % 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, jeśli obraz jest większy niż 256px na dowolnej stronie Zastosuj okno gausińskie (11x11, σ=1.5) do obliczania lokalnych statystyk Oblicz średnią (μ), zmienność (σ2) i kowariancję dla każdego okna 4 Połączenie w formule SSIM z stały stabilności C1 i C2 Zwraca średnią wszystkich lokalnych wartości SSIM Porty JavaScript Trzy części wymagają ostrożnej uwagi: okna Gaussian, oddzielna konwolucja i filtr do pobierania próbek. Gausińskie okna z MATLAB tworzy rdzeń 11x11 Gaussian. Odpowiednik 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; } Należy zauważyć, że tworzy to okno 1D, a nie 2D. Jest to celowe, ponieważ filtry Gaussian są oddzielalne, co oznacza, że konwolucja 2D może być podzielona na dwa przejścia 1D. Ten sam wynik, połowa operacji. Separable Convolution MATLAB's aplikuje rdzeń 2D. Dla okna 11x11 na obrazie 1000x1000 jest to 121 milionów mnożeń. 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; } } } Automatyczny Downsampling Odniesienie MATLAB obniża próbki obrazów większych niż 256px przy użyciu filtra pola z symetrycznym podkładaniem: 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 }; } Przekraczanie tego kroku nie złamaje nic oczywistego. nadal otrzymasz wyniki SSIM. Ale nie będą pasować do odniesienia, a algorytm będzie działał wolniej na dużych obrazach. walidacji 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]); } Tolerancja jest tu bardziej rygorystyczna. SSIM to dobrze zdefiniowany algorytm z dokładnymi współczynnikami. Jeśli mamy więcej niż 0,05%, coś jest nie tak: 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); }); Wyniki 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 % 2a vs 2b 0.912847 0.912872 0.00% 3a vs 3b 0.847621 0.847874 0,03 proc. 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 proc. 2a vs 2b 0.916523 0.912872 0,40 proc. 3a vs 3b 0.853842 0.847874 0,73 proc. 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. Różnice w ssim.js pochodzą z algorytmicznych wyborów: brak automatycznego pobierania próbek, nieco inna Gaussian approximation i różne zarządzanie granicami. Common Pitfalls Porting MATLAB to JavaScript looks straightforward until it isn't. These are the issues that cost me time. Array Indexing MATLAB arrays start at 1. JavaScript arrays start at 0. Everyone knows this. Everyone still gets bitten by it. Błąd jest rzadko oczywisty. Nie otrzymasz błędu typu off-by-one na pierwszym pikselu. Tłumaczy to Dostęp do sąsiednich becomes , który uderza w indeks -1 gdy is 0. for i = 1:height for (let i = 0; i < height; i++) img(i-1, j) img[(i-1) * width + j] i Naprawa: narysuj przykład 3x3 na papierze przed napisaniem pętli. Kolumnowy Major vs Row Major MATLAB przechowuje matryce kolumna po kolumnie. JavaScript TypedArrays przechowuje kolejno po kolejce. Matrix 3x3 w programie MATLAB: [1 4 7] [2 5 8] → stored as [1, 2, 3, 4, 5, 6, 7, 8, 9] [3 6 9] Ta sama matryca w JavaScript: [1 4 7] [2 5 8] → stored as [1, 4, 7, 2, 5, 8, 3, 6, 9] [3 6 9] To ma znaczenie przy tłumaczeniu indeksowania. MATLAB Staje się W języku JavaScript, nie . img(row, col) img[row * width + col] img[col * height + row] Większość bibliotek obrazów już przekazuje dane wierszowe, więc wszystko jest w porządku.Ale jeśli kopiujesz operacje matrycy MATLAB słowami, uważaj. działanie graniczne z MATLAB , , oraz mają różne zachowania domyślne: conv2 filter2 imfilter conv2(A, K) – bez podłoża, zmniejsza się wydajność Conv2 (A, K, 'taki sam') — zero padding, wyjście jest tego samego rozmiaru jak wejście — no padding, output shrinks filter2(K, A, 'valid') imfilter(A, K, 'symetryczny') — podkładka lustra na krawędziach Zrób to źle, a twoje wyniki będą się różnić przy każdym pikselu w pobliżu granicy. The SSIM reference uses z Podaje się go w celu obniżenia, następnie z Zapomnij o szczegółach i zastanawiasz się, dlaczego Twoje liczby są 2%. imfilter 'symmetric' filter2 'valid' Kosmiczne współczynniki kolorów Konwersja RGB do szarości wydaje się prosta. Mnożenie współczynnikami, sumowanie kanałów. Ale jakie współczynniki? z MATLAB Korzystanie z 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 Inni używają prostej średniej: Y = (R + G + B) / 3 Różnica jest nieistotna w przypadku większości obrazów. Ale jeśli walidujesz w stosunku do MATLAB, użyj dokładnych współczynników BT.601. W przeciwnym razie będziesz ścigać błędy fantomowe, które są naprawdę po prostu niepowodzeniem konwersji w skali szarości. Implicitne zachowania MATLAB MATLAB wykonuje automatycznie rzeczy, których JavaScript nie wykonuje: z MATLAB konwertuje uint8 (0-255) na float (0.0-255.0). Jeśli Twój JavaScript odczytuje dane PNG jako Uint8ClampedArray i zapomnisz konwertować, twoje obliczenia różnicy przepłyną. Auto-casting double(img) Funkcje MATLAB mają wartości domyślne zakopane w dokumentacji. , , window size 11, sigma 1.5. Miss one, and your implementation diverges. Default parameters K = [0.01, 0.03] L = 255 Zoptymalizuj swój JavaScript Dokładność przychodzi na pierwszym miejscu. ale gdy twoja implementacja pasuje do MATLAB, wydajność ma znaczenie. algorytmy naukowe przetwarzają miliony pikseli. Te optymalizacje sprawiły, że implementacja SSIM była o 25-70% szybsza niż w przypadku ssim.js przy zachowaniu dokładności. Use TypedArrays 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 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. Użyj Większość komputerów używa gdy potrzebujesz dodatkowej precyzji (akumulacja sum na dużych obrazach). do ostatecznego wydania obrazu. Float32Array Float64Array Uint8ClampedArray oddzielne filtry Konwolucja 2D z rdzeniem NxN wymaga mnożenia N2 na piksel. Ale filtry Gausian są oddzielne. Gausian 2D jest produktem zewnętrznym dwóch Gausianów 1D. Zamiast jednego przejścia 11x11, wykonujesz dwa przejścia 11x1: 22 operacje na piksel. // 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; } } Działa to dla dowolnego oddzielnego rdzenia: Gaussian, Box Filter lub Sobel. Sprawdź, czy rdzeń jest oddzielny przed wdrożeniem konwolucji 2D. Reuse 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, I allocate all buffers upfront: grayscale images, squared images, filtered outputs, and the SSIM map. One allocation at the start, zero during computation. Cache obliczonych wartości Niektóre wartości są ponownie wykorzystywane. Nie obliczaj ich dwa razy. // 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; } Okno Gaussian dla SSIM jest zawsze 11x11 z σ = 1.5. Obliczanie zajmuje mikrosekundy. Unikaj sprawdzania granic w gorących łańcuchach 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. Wyniki Benchmarkowane na tych samych obrazach, tej samej maszyny: Implementation Time (avg) vs MATLAB accuracy ssim.js 86ms 0.05-0.73% @blazediff/ssim 64ms 0.00-0.03% Szybki.js 86ms 0.05-0.73% @blazediff/ssim 64 ms 0,00 do 0,03% 25% szybsza i dokładniejsza. Nie ze względu na inteligentne algorytmy – ze względu na staranną inżynierię. TypedArrays, oddzielne filtry, ponowne użycie bufora, okna w pamięci podręcznej. Dla tych wariant (używa integralnych obrazów zamiast konwolucji), przepaść jest szersza: 70% szybsza niż ssim.js na dużych obrazach. Szym Hitchhikera Takeaways Porting scientific algorithms from MATLAB to JavaScript is mechanical once you have the proper process. . 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 Z archiwum . Use the reference implementation .m Jest bezpłatny, działa wszędzie i wykonuje kod MATLAB bez modyfikacji. Zainstaluj testy podstawowej prawdy na wczesnym etapie. Uruchom je w CI. Kiedy liczby pasują do 4 miejsc dziesiętnych, jesteś gotowy. Validate with Octave . Get your implementation matching MATLAB before optimizing. A fast wrong answer is still wrong. Once you're accurate, the optimizations are straightforward: TypedArrays, separable filters, buffer reuse. Accuracy first, then performance Czasami nie zgadzasz się dokładnie. obsługa granic, precyzja pływających punktów i celowe uproszczenia. To jest w porządku. Wiedz, dlaczego się różnisz i ile. Napisz to. 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. Wdrożenia SSIM i GMSD z tego artykułu są dostępne pod adresem [blazediff.dev] (https://blazediff.dev). licencjonowane przez MIT, zero zależności, zatwierdzone wobec MATLAB.