Проблемот Повеќето научни алгоритми живеат во - обработка на слики и анализа, основи на машинско учење, итн Истражувачите пишуваат MATLAB, објавуваат статии и споделуваат датотеки . Матлаб .m Но, производствениот код врз основа на овие алгоритми работи на друго место. Ако градите веб-апликација која има потреба од метрики за квалитетот на сликата, пишувате JavaScript. Значи, ќе најдете хартија, ќе го прочитате алгоритмот и повторно ќе го имплементирате. Еве го проблемот: како знаете дека вашата имплементација е точна? Можете да напишете единици тестови. Проверете дали идентични слики се враќаат 1.0, и дека различни слики се враќаат пониска вредност. Но, тоа само фаќа очигледни грешки. Суптилните се погрешно гранично ракување, малку од коефициентите, или недостасуваат чекори за нормализација. Вашиот код работи и враќа веродостојни броеви, но тоа не го прави она што го опишува документот. Постоечките библиотеки на JavaScript имаат овој проблем. Тие пренесуваат други јазични порти на ниско ниво, секоја генерација се движи подалеку од оригиналот.Никој не валидира против референцата MATLAB бидејќи MATLAB чини пари, а извршувањето на тоа во CI не е тривијално. Сакав веродостојни имплементации на SSIM (Структурален индекс на сличност) и GMSD (Градиентна големина на сличност). Опциите за JavaScript биле бавни, неточни или и двете. Идеја: Користете Octave за верификација е бесплатна, со отворен код алтернатива на MATLAB. датотеки, произведува исти излези, и инсталира на било кој CI сервер. ГНУ Октава .m Преземете ја оригиналната MATLAB имплементација од хартија Извршете го во Octave со тест слики Извршете ја вашата JavaScript имплементација со истите слики Споредете ги резултатите Ова не е тестирање на единици. Тоа е валидација на вистината. Вие не проверувате дали вашиот код се однесува правилно според вашето разбирање. Вие проверувате дали точно одговара на референтната имплементација. Еве како изгледа во пракса: 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]); } Можете да го повикате Octave од Node.js, да го анализирате резултатот од пловечката точка и да го споредите со вашиот JavaScript излез. Започнете едноставно: GMSD GMSD ја мери сличноста на сликата користејќи градиенти. Тоа ги пресметува информациите за работ за двете слики, ги споредува пиксели по пиксели и враќа еден резултат. Алгоритмот се базира на Таа е едноставна, добро документирана и нема постоечка имплементација на JavaScript. Xue, W., Zhang, L., Mou, X., & Bovik, A. C. (2013). "Градиентна сличност со сличност со големина: високо ефикасен перцептивен индекс за квалитет на сликата." MATLAB референца Постојат 35 линии: оригинална имплементација 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 Намалување на двете слики со 2x со користење на просечен филтер Пресметка на градиентните величини со Prewitt оператори (3x3 edge detection) Да се пресмета сличноста по пиксел 4.Врати стандардно отстапување на сличноста на мапата Портот на JavaScript Основната идеја: сликите кои изгледаат слично имаат слични рабови. Фотографијата и нејзината малку компресирана верзија имаат рабови на истите места. GMSD го фаќа ова со пресметување на "градиентната големина" - во суштина, колку се силни рабовите на секој пиксел. Тој го користи операторот Prewitt, класичен 3x3 филтер кој ги детектира хоризонталните и вертикалните промени во интензитетот. . sqrt(horizontal² + vertical²) Во MATLAB, ова е еден линеар со Во JavaScript, ние се движиме низ секој пиксел и неговите 8 соседи: 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; } Откако ќе имате големини на градиент за двете слики, GMSD ги споредува пиксели по пиксели и го враќа стандардното отстапување на разликите. 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); } Валидација Ова е местото каде што Octave го добива своето држење. Ние го извршуваме оригиналниот MATLAB код и нашата имплементација на JavaScript на истите слики и ги споредуваме излезите. 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]); } Прекинете го ова надолу: addpath – му кажува на Octave каде да го најде GMSD.m (оригиналниот MATLAB датотека од хартија) imread — ги вчита тестовите слики rgb2gray - конвертира во сива скала ако е потребно (GMSD работи само на осветлување) двојна() – MATLAB/Octave бара влез на пловечка точка, а не uint8 fprintf('%.15f') – излегува 15 десетични места, доволно прецизно за да ги фати суптилните грешки Octave отпечатува некои информации за стартување, па само го фаќаме резултатот од пловечката точка. Користење на Vitest, но Jest или било која друга рамка работи исто: 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); }); }); Клучот е во поставувањето на вистинската толеранција. Премногу строги (0.0001%) и ќе ги прогонувате духовите од пловечките точки. Премногу лабава (10%) и вистински грешки се пробиваат. Се спуштив на 2% за GMSD откако ги разбрав изворите на разликите (повеќе за тоа во Резултатите). За CI, Octave инсталира во секунди на 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 Секој притисок сега го валидира вашиот JavaScript против референцата MATLAB. Ако случајно ги прекршите границите на ракување или се мешате со коефициентот, тестот не успева. 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 vs 1b 0.088934 0.089546 од 0,68% 2a vs 2b 0.142156 0.143921 1.23 отсто 3а против 3б 0.067823 0.068412 0,86 проценти Разликата од 0,68-1,23% доаѓа од граничното ракување во од Matlab како перцептивна метрика за квалитет, оваа разлика е прифатлива бидејќи релативните рангирања на парови на слики остануваат идентични. conv2 'same' Скалирање на: SSIM SSIM (Structural Similarity Index) is the industry standard for image quality assessment. It compares luminance, contrast, and structure between two images and returns a score from 0 to 1. Higher means more similar. 1.0 means identical. The algorithm comes from Евалуација на квалитетот на сликата: од видливоста на грешките до структурната сличност. Широко се цитира, добро се разбира и веќе има JavaScript имплементации. компликацијата е дека не почнуваме од нула. Влегуваме во простор со постоечките библиотеки, така што можеме директно да ја споредиме точноста. Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P (2004) IEEE трансакции за обработка на слики Постоечкиот пејзаж The most popular JavaScript implementation is Тоа функционира.Враќа веродостојни броеви.Но никој не го потврди против референцата MATLAB. СЈО.ЈС Ја извршив споредбата.Резултатите беа изненадувачки: Library Difference from MATLAB ssim.js 0.05%-0.73% @blazediff/ssim 0.00%-0.03% СЈО.ЈС 0.05%-0.73% @Блазедиф / Сим 0.00% од 0,03% Тоа е до 24 пати попрецизно. Не затоа што ssim.js е лошо напишан – тоа е разумна имплементација. Но, малите одлуки се акумулираат: малку различни Gaussian прозорци, недостасува авто-downsampling, различно ракување со границите. MATLAB референца Референтната имплементација на Ванг е покомплексна од GMSD. Клучниот додаток е автоматско намалување на примероците. Големите слики се намалуваат пред споредбата. Ова одговара на човечката перцепција (не забележуваме еднопикселни разлики во 4K слика) и го подобрува перформансот. Основни компјутери : % 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: Пониски примероци ако сликата е поголема од 256px на која било страна Применување на Гауски прозорец (11x11, σ=1.5) за пресметување на локалната статистика Пресметнете го просекот (μ), варијацијата (σ2) и коваријансата за секој прозорец 4 Комбинирајте ја формулата SSIM со константите за стабилност C1 и C2 Вратете го просекот на сите локални SSIM вредности Портот на JavaScript Три делови бараат внимателно внимание: Гауски прозорци, одвојувачка конволуција и филтерот за намалување на примерокот. Gaussian Windows MATLAB's создава 11x11 Gaussian јадро. 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; } Забележете дека ова создава 1D прозорец, а не 2D прозорец.Тоа е намерно, бидејќи Гаузиските филтри се одвојуваат, што значи дека 2D конволуцијата може да се подели на два 1D пасоши. Сепаративна конверзија од Matlab применува 2D јадро. За 11x11 прозорец на 1000x1000 слика, тоа е 121 милиони множења. 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; } } } Автоматско намалување Референцата на MATLAB ги намалува сликите поголеми од 256px со користење на филтер за кутии со симетрично прицврстување: 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 }; } Прескокнувањето на овој чекор не прекинува ништо очигледно. Сепак добивате SSIM резултати. Но тие нема да одговараат на референцата, а алгоритмот ќе работи побавно на големи слики. Валидација Истиот модел како GMSD. Повик Octave, споредба на резултатите: 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]); } Толеранцијата е построга тука. SSIM е добро дефиниран алгоритам со точни коефициенти. Ако имаме повеќе од 0,05% попуст, нешто не е во ред: 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); }); Резултати 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 проценти 2а против 2б 0.912847 0.912872 0,00 проценти 3а против 3б 0.847621 0.847874 0,03 % од Во споредба со ssim.js на истите слики: 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% 1а против 1б 0.969241 0.968755 0.05% 2а против 2б 0.916523 0.912872 0,40 отсто 3а против 3б 0.853842 0.847874 0.73% 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. Разликите на ssim.js доаѓаат од алгоритамски избори: нема автоматско намалување на примерокот, малку поинаков Гаусиан приближување, и различни граници ракување. Common Pitfalls Porting MATLAB to JavaScript looks straightforward until it isn't. These are the issues that cost me time. Индексирање на матрицата MATLAB матриците започнуваат на 1. JavaScript матриците започнуваат на 0. Сите го знаат ова. Грешката ретко е очигледна. Нема да добиете грешка од еден по еден на првиот пиксел. Ќе добиете малку погрешни вредности на границите на сликата, каде што кругови како Преведување на Достапност на соседите станува , кој го погоди индексот -1 кога Тоа е 0. for i = 1:height for (let i = 0; i < height; i++) img(i-1, j) img[(i-1) * width + j] i Поправка: Нацртајте пример од 3x3 на хартија пред да го напишете кругот. Column-Major vs Row-Major MATLAB ги складира матриците колона по колона. JavaScript TypedArrays store row-by-row. 3x3 матрица во 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] This matters when you translate indexing. MATLAB's станува in JavaScript, not . 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. Boundary Handling од Matlab , на , and have different default behaviors: conv2 filter2 imfilter — no padding, output shrinks conv2(A, K) conv2 (A, K, 'само') — нула падење, излез иста големина како влез — no padding, output shrinks filter2(K, A, 'valid') имфилтер(A, K, 'симетричен') — огледало падење на рабовите Направете го ова погрешно, и вашите резултати ќе се разликуваат на секој пиксел во близина на границата. За слика од 1000x1000 со 11x11 јадро, тоа е ~4% од вашите пиксели. The SSIM reference uses со padding for downsampling, then со 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 Конвертирањето на RGB во сива скала изгледа едноставно. Помножете го со коефициенти, збијте ги каналите. Но кои коефициенти? MATLAB's uses BT.601: rgb2gray Y = 0.298936 * R + 0.587043 * G + 0.114021 * B Некои библиотеки на JavaScript користат BT.709: Y = 0.2126 * R + 0.7152 * G + 0.0722 * B Други користат едноставен просек: Y = (R + G + B) / 3 Разликата е незначителна за повеќето слики. Но, ако се валидирате против MATLAB, користете ги точните коефициенти BT.601. Имплицитно однесување на MATLAB MATLAB does things automatically that JavaScript won't: : MATLAB's конвертира uint8 (0-255) во float (0.0-255.0). Ако вашиот JavaScript ги чита податоците од PNG како Uint8ClampedArray и заборавите да конвертирате, вашите пресметки за варијанса ќе бидат преплавени. Auto-casting double(img) MATLAB функциите имаат стандардни вредности закопани во нивната документација. , , големина на прозорецот 11, sigma 1.5. пропушти еден, и вашата имплементација се разликува. Default parameters K = [0.01, 0.03] L = 255 Оптимизирање на вашиот JavaScript Точноста доаѓа на прво место.Но, откако вашата имплементација ќе се совпадне со MATLAB, перформансите се важни.Научните алгоритми обработуваат милиони пиксели. These optimizations made my SSIM implementation 25-70% faster than ssim.js while maintaining accuracy. Користете типови Редовните масиви на JavaScript се флексибилни. Тие можат да содржат мешани типови, да растат динамично и да имаат погодни методи. // 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 имаат фиксна големина, фиксен тип и континуирана меморија. JavaScript моторот знае точно како да ги оптимизира. Use for most computations. Use when you need extra precision (accumulating sums over large images). Use за конечен извод на сликата. Float32Array Float64Array Uint8ClampedArray Separable Filters 2D конволуција со NxN јадро бара N2 множења по пиксел. За SSIM 11x11 Gaussian, тоа е 121 операции по пиксел. 2D Gaussian е надворешен производ на два 1D Gaussians. Наместо еден 11x11 пасош, правите два 11x1 пасоши: 22 операции по пиксел. // 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; } } Ова работи за секое раздвојувачко јадро: Gaussian, box filter, или Sobel. Проверете дали вашето јадро е раздвојувачко пред да имплементирате 2D конволуција. Повторно користење на буферите Доделувањето на меморијата е скапо. Собирањето на ѓубрето е полошо. Доделувајте еднаш, повторно користете секаде. // 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. Креирање на компјутеризирани вредности Некои вредности се користат повторно. Не ги пресметувајте двапати. // 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; } Гаузискиот прозорец за SSIM е секогаш 11x11 со σ = 1.5. За пресметување се потребни микросекунди. но кога се обработуваат илјадници слики, микросекунди се додаваат. Избегнувајте да ги проверувате границите во топли кругови 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 Но, за слика од 1000x1000, ги отстранувате милиони условни чекови. Results Бенчмаркирани на истите слики, истата машина: Implementation Time (avg) vs MATLAB accuracy ssim.js 86ms 0.05-0.73% @blazediff/ssim 64ms 0.00-0.03% СЈО.ЈС 86ms 0.05 0,73% од вкупната вредност @blazediff/ssim 64 МС 0 0 0 0 0 0 0 0 0 0 25% побрзо и попрецизно. Не поради паметни алгоритми – поради внимателно инженерство. Типови, одвојувачки филтри, повторна употреба на буферот, кеширани прозорци. За на variant (uses integral images instead of convolution), the gap is wider: 70% faster than ssim.js on large images. Hitchhiker's SSIM Преземање Портирањето на научни алгоритми од MATLAB на JavaScript е механички кога ќе го имате вистинскиот процес. Документите ги опишуваат алгоритмите. MATLAB кодот ги имплементира. Овие не се исто. случаите на раб, параметрите по дефолт, ракувањето со границите - тие се во кодот, а не на хартијата. Започнете со file. Use the reference implementation .m Тоа е бесплатно, работи насекаде и го извршува MATLAB кодот без модификации. Поставете ги тестовите на вистината рано. Извршете ги во CI. Кога вашите броеви одговараат на 4 десетични места, сте завршени. Кога тие не, веднаш знаете. 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 Понекогаш нема точно да се совпадне. гранична обработка, прецизност на пловечки точки и намерно поедноставување. Тоа е во ред. Знам зошто се разликувате и со колку. Напиши го тоа. Document your differences Разликата помеѓу академскиот MATLAB и производството на JavaScript е помала отколку што изгледа. Алгоритмите се исти. Математиката е иста. Потребен ви е начин да проверите дали сте го добиле правилно. SSIM и GMSD имплементации од овој напис се достапни на [blazediff.dev] (https://blazediff.dev). MIT лиценцирани, нула зависности, валидирани против MATLAB.