Problem Većina znanstvenih algoritama živi u - obrada i analiza slika, osnove strojnog učenja itd. Istraživači pišu MATLAB, objavljuju radove i dijele datoteka . Matlab .m Ali proizvodni kod koji se temelji na tim algoritmima radi negdje drugdje. Ako gradite web aplikaciju koja treba metriku kvalitete slike, pišete JavaScript. Dakle, pronađete papir, pročitajte algoritam i ponovno ga implementirate. Možete napisati testove jedinica. Provjerite da li identične slike vraćaju 1.0, a da različite slike vraćaju nižu vrijednost. Ali to samo hvata očite greške. Subtilne su pogrešna granična rukovanja, malo off koeficijente, ili nedostaju korake normalizacije. Vaš kod radi i vraća vjerodostojne brojeve, ali to ne čini ono što papir opisuje. Postojeće knjižnice JavaScript-a imaju ovaj problem.Porte druge jezike niske razine, a svaka generacija odlazi dalje od izvornog.Nitko ne potvrđuje MATLAB referenciju jer MATLAB košta novac, a rad u CI-u nije trivijalan. Želio sam pouzdane implementacije SSIM-a (Structural Similarity Index) i GMSD-a (Gradient Magnitude Similarity Deviation). Ideja: Koristite Octave za provjeru je besplatna, otvorena alternativa MATLAB-u. datoteke, proizvodi iste izlaze i instalira se na bilo kojem CI poslužitelju. GNU Octave .m Uzmite originalnu MATLAB implementaciju iz papira Pokrenite ga u Octaveu s testnim slikama Pokrenite JavaScript implementaciju s istim slikama Usporedite rezultate Ovo nije test jedinice. To je potvrda temeljne istine. Ne provjeravate da li se vaš kod ponaša ispravno prema vašem razumijevanju. Provjeravate da li se točno podudara s referentnom implementacijom. Evo kako to izgleda u praksi: 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]); } Nazovite Octave iz Node.js-a, analizirajte rezultat plutajuće točke i usporedite ga s izlazom JavaScript-a. Starting Simple: GMSD GMSD mjeri sličnost slike pomoću gradijenata. Izračunava informacije o rubovima za obje slike, uspoređuje ih pixel-by-pixel i vraća jednu ocjenu. Algoritam se temelji na Jednostavno je, dobro dokumentirano i nema postojeće implementacije JavaScript-a. Xue, W., Zhang, L., Mou, X., & Bovik, A. C. (2013). „Gradijentno odstupanje sličnosti veličine: visoko učinkovit indeks kvalitete percepcije slike.“ IEEE Transactions on Image Processing Matlab referencija Ukupno 35 linija: Originalna provedba 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 both images by 2x using an averaging filter Izračunajte veličine gradijenata s Prewittovim operatorima (3x3 detekcija rubova) Izračunajte sličnost po pikselu 4. vratite standardno odstupanje karte sličnosti The JavaScript Port The core idea: images that look similar have similar edges. A photo and its slightly compressed version have edges in the same places. A photo and a completely different photo don't. GMSD to sakuplja izračunavajući "gradientnu veličinu" - u osnovi, koliko su jake rubovi na svakom pikselu. Koristi Prewitt operator, klasični 3x3 filter koji otkriva horizontalne i vertikalne promjene intenziteta. veličina kombinira oba smjera: . sqrt(horizontal² + vertical²) U MATLAB-u, ovo je one-liner s U javascript-u prolazimo kroz svaki piksel i njegovih 8 susjeda: 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; } Nakon što imate veličine gradijenata za obje slike, GMSD ih uspoređuje piksel po pikselu i vraća standardno odstupanje razlika. 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); } Validacija Ovdje Octave dobiva svoje držanje. Mi pokrećemo izvorni MATLAB kod i našu implementaciju JavaScript na istim slikama i usporedimo izlaze. 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]); } Razbiti ovo dolje: addpath – kaže Octave gdje pronaći GMSD.m (originalna MATLAB datoteka iz papira) imread — učitava test slike rgb2gray – pretvara se u ljestvicu sive boje ako je potrebno (GMSD radi samo na svjetlosti) double() – MATLAB/Octave zahtijeva unos plutajuće točke, a ne uint8 fprintf('%.15f') – izlazi 15 decimalnih mjesta, dovoljno precizno da uhvati suptilne bugove Octave štampa neke start-up informacije, tako da smo uhvatiti samo rezultat plutajuće točke. Koristeći Vitest, ali Jest ili bilo koji drugi okvir radi isto: 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); }); }); Ključ je u postavljanju prave tolerancije. Previše strogo (0.0001%) i pratit ćete duhove plutajuće točke. Previše slabe (10%) i prave greške prolaze kroz. Srušio sam se na 2% za GMSD nakon razumijevanja izvora razlika (više o tome u rezultatima). Za CI, Octave instalira u sekundama 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 Svaki pritisak sada potvrđuje vaš JavaScript protiv MATLAB reference. Ako slučajno prekinete granicu rukovanja ili zbunite koeficijent, test ne uspijeva. Results 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 protiv 1b 0.088934 0.089546 0,68 posto 2a i 2b 0.142156 0.143921 1.23 posto 3a protiv 3b 0.067823 0.068412 0.86% 0,68-1,23% razlika dolazi od graničnog rukovanja u Matlab je Modus obrađuje rubove slike na drugačiji način od moje implementacije.Kao metrika perceptualne kvalitete, ta je razlika prihvatljiva jer relativni rangovi parova slika ostaju identični. conv2 'same' Scaling Up: SSIM SSIM (Structural Similarity Index) je industrijski standard za procjenu kvalitete slike. On uspoređuje svjetlost, kontrast i strukturu između dvije slike i vraća ocjenu od 0 do 1. Algoritam dolazi od Procjena kvalitete slike: od vidljivosti pogreške do strukturne sličnosti. Široko je citirana, dobro razumljiva i već ima JavaScript implementacije. Komplikacija je da ne počinjemo od nule. Ulazimo u prostor s postojećim knjižnicama, tako da možemo izravno usporediti točnost. Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P (2004) IEEE transakcije na obrađivanju slika Postojeći krajolik Najpopularnija JavaScript implementacija je Vraća vjerodostojne brojeve, ali nitko ga nije validirao protiv MATLAB reference. Sljedeći članakSsim.js Uspio sam napraviti usporedbu, a rezultati su bili iznenađujući: Library Difference from MATLAB ssim.js 0.05%-0.73% @blazediff/ssim 0.00%-0.03% Sljedeći članakSsim.js 0.05%-0.73% @Blazediff / Sijam 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. Matlab referencija Wangova referentna implementacija je složenija od GMSD-a. Ključni dodatak je automatsko uzorkovanje. Velike slike se smanjuju prije usporedbe. To odgovara ljudskoj percepciji (ne primjećujemo razlike pojedinačnih piksela u 4K slikama) i poboljšava performanse. Osnovno računanje : % 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: Ako je slika veća od 256 px na bilo kojoj strani Apply a Gaussian window (11x11, σ=1.5) to compute local statistics Izračunajte prosjek (μ), varijancu (σ2) i kovarijancu za svaki prozor 4 Kombinacija u SSIM formulu sa konstantima stabilnosti C1 i C2 Vraća sredinu svih lokalnih SSIM vrijednosti JavaScript vrata Tri dijela zahtijevaju pažljivu pozornost: Gaussian prozori, odvojive konvulacije, i filtriranje uzorkovanja. Gaussian Windows Matlab je stvara 11x11 Gaussian kernel. JavaScript ekvivalent: 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; } Imajte na umu da to stvara 1D prozor, a ne 2D. To je namjerno jer su Gaussian filtri odvojivi, što znači da se 2D konvulacija može podijeliti u dva 1D prolaza. Odvojena konvulzija Matlab je applies a 2D kernel. For an 11x11 window on a 1000x1000 image, that's 121 million multiplications. Separable convolution cuts it to 22 million: 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; } } } Automatsko snižavanje MATLABov referentni uzorak donosi slike veće od 256 px pomoću polja filtera s simetričnim oblogom: 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 }; } Preskakanje ovog koraka ne narušava ništa očito. još uvijek dobivate SSIM ocjene. Ali oni se neće podudarati s referencijom, a algoritam će se sporije pokrenuti na velikim slikama. Validacija Isti uzorak kao i GMSD. Nazovite Octave, usporedite rezultate: 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]); } Tolerancija je ovdje stroža. SSIM je dobro definiran algoritam s točnim koeficijentima. Ako smo više od 0,05% off, nešto nije u redu: 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); }); Rezultati 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 protiv 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% U usporedbi s ssim.js na istim slikama: 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 protiv 1b 0.969241 0.968755 0,05 posto 2a vs 2b 0.916523 0.912872 0,40 posto 3a protiv 3b 0.853842 0.847874 0,73 posto 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. Razlike ssim.js dolaze od algoritamskih izbora: bez automatskog uzorkovanja, malo drugačijeg Gaussian približavanja i različitog upravljanja granicama. Uobičajeni pitfalls Portiranje MATLAB-a u JavaScript izgleda jednostavno dok to nije. Array indeksiranje MATLAB serije počinju na 1. JavaScript serije počinju na 0. Svi to znaju. Bug je rijetko očigledan. Nećete dobiti pogrešku od jednog do drugog na prvom pikselu. translate to but the neighbor access Postaje , which hits index -1 when is 0. for i = 1:height for (let i = 0; i < height; i++) img(i-1, j) img[(i-1) * width + j] i Popravak: nacrtati primjer 3x3 na papiru prije pisanja zavoja. Provjerite svoje granice uvjete izričito. Sljedeći članakMajor protiv Row-Major MATLAB pohranjuje matrice kolona po koloni. JavaScript TypedArrays pohranjuje reda po redu. 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] The same matrix in JavaScript: [1 4 7] [2 5 8] → stored as [1, 4, 7, 2, 5, 8, 3, 6, 9] [3 6 9] To je važno kada prevodite indeksiranje. MATLAB Postaje in JavaScript, not . img(row, col) img[row * width + col] img[col * height + row] Većina knjižnica slika već vam šalje glavne podatke, tako da je sve u redu.Ali ako kopirate MATLAB operacije matrice verbatim, pazite. Granica djelovanja Matlab je , i Postoje različiti tipovi ponašanja: conv2 filter2 imfilter conv2 (A, K) – nema podnošenja, izlaznost se smanjuje — zero padding, output same size as input conv2(A, K, 'same') filter2(K, A, 'validan') – nema podnošenja, isporuka se smanjuje imfilter(A, K, 'simetrični') — ogledalo za podmazivanje na rubovima Učinite to pogrešno, a rezultati će se razlikovati na svakom pikselu u blizini granice. Za sliku od 1000x1000 s jezgrom od 11x11, to je ~4% vaših piksela. SSIM referencija koristi s Sljedeći Članak Padding za downsampling 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 Converting RGB to grayscale seems simple. Multiply by coefficients, sum the channels. But which coefficients? Matlab je uses 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 Razlika je zanemariva za većinu slika. Ali ako validirate u odnosu na MATLAB, koristite točne BT.601 koeficijente. U protivnom, pratit ćete bugove koji su zapravo samo neusklađenost s konverzijom u sivi stupanj. MATLAB-ovo implicitno ponašanje 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 funkcije imaju podrazumijevane vrijednosti zakopane u njihovu dokumentaciju. , , veličina prozora 11, sigma 1.5. propustite jedan, a vaša implementacija se razlikuje. Default parameters K = [0.01, 0.03] L = 255 Optimizing Your JavaScript Preciznost dolazi na prvo mjesto.Ali kada se vaša implementacija podudara s MATLAB-om, značajna je performansa.Znanstveni algoritmi obrađuju milijune piksela. Ove optimizacije učinile su moju implementaciju SSIM-a 25-70% bržom od ssim.js, uz održavanje točnosti. 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. Koristiti za većinu računala. koristiti kada je potrebna dodatna preciznost (akumuliranje iznosa na velikim slikama). for final image output. Float32Array Float64Array Uint8ClampedArray Odvojeni filtri 2D konvulacija s NxN jezgrom zahtijeva N2 množenje po pikselu. 2D Gaussian je vanjski proizvod dva 1D Gaussiana. Umjesto jednog 11x11 prolaza, radite dva 11x1 prolaza: 22 operacija po pikselu. // 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; } } To djeluje za bilo koji odvojivi jezgre: Gaussian, filter za kutije, ili Sobel. Provjerite je li vaš jezgre odvojivo prije implementiranja 2D konvolucije. Preuzimanje buffera Dodjela memorije je skupo. sakupljanje smeća je gore. Dodijeliti jednom, ponovno korištenje svugdje. // 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 } } Za SSIM, dodjeljujem sve buffere unaprijed: slike sive ljestvice, kvadratne slike, filtrirane izlaze i SSIM mapu. Cache izračunane vrijednosti Neke vrijednosti se ponovno koriste.Ne izračunavajte ih dvaput. // 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; } Gaussian prozor za SSIM je uvijek 11x11 s σ=1.5. Računanje traje mikrosekunde. Avoid Bounds Checking in Hot Loops Za unutarnje piksele u kojima znate da su indeksi valjani, to je uzaludni rad. // 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. Rezultati Benchmarked na istim slikama, isti stroj: Implementation Time (avg) vs MATLAB accuracy ssim.js 86ms 0.05-0.73% @blazediff/ssim 64ms 0.00-0.03% Sljedeći članakSsim.js 86ms 0,05 – 0,7 % @blazediff/ssim 64 milijuna 0.00-0.03% 25% brže i točnije. Ne zbog pametnih algoritama – zbog pažljivog inženjeringa.TipedArrays, odvojivi filtri, ponovna uporaba buffera, cache prozori. Za The varijanta (koristi integralne slike umjesto konvolucije), razlika je šira: 70% brža od ssim.js na velikim slikama. Hitchhikerov SSIM Održavanje Portiranje znanstvenih algoritama iz MATLAB-a u JavaScript je mehaničko kada imate odgovarajući proces. . 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 file. Use the reference implementation .m Besplatno je, pokreće se svugdje i izvodi MATLAB kod bez izmjena. Uspostavite testove temeljne istine rano. Pokrenite ih u CI. Kada se vaši brojevi podudaraju s 4 decimalna mjesta, završite. Validate with Octave Preuzmite svoju implementaciju koja odgovara MATLAB-u prije optimizacije. Brzi pogrešan odgovor je još uvijek pogrešan. Nakon što ste točni, optimizacije su jednostavne: tipkanjeArrays, odvojivi filtri, ponovna uporaba buffera. Accuracy first, then performance Ponekad nećete točno odgovarati. granično rukovanje, preciznost plutajućih točaka i namjerna pojednostavnjenja. To je u redu. Znati zašto se razlikujete i koliko. Document your differences Razlika između akademskog MATLAB-a i produkcijskog JavaScript-a manja je nego što se čini. Algoritmi su isti. Matematika je ista. Potreban vam je način da provjerite da li je to ispravno. SSIM i GMSD implementacije iz ovog članka dostupne su na [blazediff.dev] (https://blazediff.dev). MIT licenciran, nula ovisnosti, validiran prema MATLAB-u.