Problem Most scientific algorithms live in 画像処理と分析、機械学習の基礎知識など、研究者はMATLABを書く、論文を発表し、共有する。 ファイル MATLAB .m しかし、これらのアルゴリズムに基づく生産コードは他の場所で実行されます。画像品質指標が必要なウェブアプリケーションを構築している場合は、JavaScriptを書いています。 ユニットテストを書くことができます。 同一の画像が 1.0 を返すか、異なる画像が低い値を返すかを確認します。 しかし、それは明らかなバグだけを捕獲します。 微妙なものは間違った境界処理、わずかに割り当ての割り当て、または正常化のステップが欠けています。 あなたのコードは実行され、信頼できる数値を返しますが、それは論文で説明されていることを行いません。 既存のJavaScriptライブラリにはこの問題があります。彼らは他の低レベルの言語ポートをポートし、各世代はオリジナルからさらに遠ざかっています。 I ran into this when working on image comparison tools. I wanted reliable implementations of SSIM (Structural Similarity Index) and GMSD (Gradient Magnitude Similarity Deviation). The JavaScript options were either slow, inaccurate, or both. So I ported them myself and built a validation system to prove they match MATLAB. アイデア: 検証のために Octave を使う is a free, open-source alternative to MATLAB. It runs the same 同じ出力を作成し、任意の CI サーバーにインストールします。 GNU Octave .m 最初の MATLAB 実装を紙から取り出します。 Run it in Octave with test images 同じ画像でJavaScript実装を実行する 結果を比較 This isn't unit testing. It's ground-truth validation. You're not checking that your code behaves correctly according to your understanding. You're checking that it matches the reference implementation exactly. Here's what that looks like in practice: 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. 曖昧さを損なうことなく - 15 decimal places を取る。 スタートシンプル: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. このアルゴリズムは、 シンプルでドキュメンタリーで、既存のJavaScript実装がありません。ポータリングプロセスを示すクリーンなスレット。 Xue, W., Zhang, L., Mou, X., & Bovik, A. C. (2013). "Gradient Magnitude Similarity Deviation: A Highly Efficient Perceptual Image Quality Index." IEEE Transactions on Image Processing MATLAB 参照 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 Downsample both images by 2x using an averaging filter Compute gradient magnitudes with Prewitt operators (3x3 edge detection) ピクセルごとの類似性を計算 4. Return the standard deviation of the similarity map 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 captures this by computing "gradient magnitude" - essentially, how strong the edges are at each pixel. It uses the Prewitt operator, a classic 3x3 filter that detects horizontal and vertical intensity changes. The magnitude combines both directions: . sqrt(horizontal² + vertical²) In MATLAB, this is a one-liner with 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); } Validation ここで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 — loads the test images テスト画像 rgb2gray — 必要に応じてグレイスケールに変換します(GMSDは明るさのみで動作します) double() — MATLAB/Octave は uint8 ではなく、浮動点入力が必要です。 fprintf('%.15f') - 出力は 15 桁、精度は微妙なバグを捕獲するのに十分 regex は Octave の出力から番号を抽出します 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%) リアルなバグが抜け落ちます 私は、違いの源を理解した後、GMSDの2%に着陸しました (結果でそれについてもっと)。 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 Every push now validates your JavaScript against the MATLAB reference. If you accidentally break boundary handling or mess up a coefficient, the test fails. No more "it looks about right." 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% 3A vs 3B 0.067823 0.068412 0.86% 0.68-1.23%の差は、境界処理によるものである。 MATLABの モードは、私の実装とは異なり画像のエッジを処理します. Perceptual quality metric として、この違いは、画像のカップルの相対的なランキングが同一であるため、受け入れられます。 conv2 'same' スケールアップ:SSIM SSIM(Structural Similarity Index)は、画像品質評価のための業界標準で、2つの画像間の明るさ、対比、構造を比較し、0から1までのスコアを返します。 このアルゴリズムは、 画像の品質評価:エラーの可視性から構造的類似性まで広く引用され、よく理解され、すでにJavaScriptの実装を有しています。複雑さは、私たちはゼロから始まっていないことです。 Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P (2004) IEEE イメージ処理に関するトランザクション 既存の風景 最も人気のあるJavaScriptの実装は 正確な数値を返しますが、誰も MATLAB 参照に反して検証していません。 SSIM.js 調べてみたところ、結果は驚きました: Library Difference from MATLAB ssim.js 0.05%-0.73% @blazediff/ssim 0.00%-0.03% SSIM.js 0.05% 0.73% ブレイク/SIM 0.00% - 0.3% 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 参照 Wang の参照実装は GMSD より複雑です. キーの追加は自動ダウンサンプルです. 大きな画像は比較前にスケールダウンされます. これは人間の感覚に匹敵します (4K 画像の 1 ピクセル差は見られません) とパフォーマンスを向上させます. コアコンピュータ: % 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 より大きい場合のダウンサンプル Gaussian ウィンドウ (11x11, σ=1.5) を適用して、局所統計を計算します。 各ウィンドウの平均(μ)、変数(σ2)および変数を計算する SSIM 公式に、安定性常数 C1 および C2 を組み合わせる すべての現地 SSIM 値の平均値を返します。 JavaScript ポート 注意が必要な3つの部分:ガウシアの窓、分離可能なコボレーション、およびサンプリングフィルター。 Gaussian Windows MATLABの creates an 11x11 Gaussian kernel. The JavaScript equivalent: 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; } これは、ガウスフィルターが分離可能であるため、つまり2Dコンボレーションは2つの1Dパスに分割することができます。 別々のコンバージョン MATLABの 2D カーネルを適用します. 1000x1000 画像の 11x11 ウィンドウの場合、それは 121 万倍です. Separable convolution は 22 万倍に切ります: 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]); } The tolerance is tighter here. SSIM is a well-defined algorithm with exact coefficients. If we're more than 0.05% off, something's wrong: 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% 2a vs 2b 0.912847 0.912872 0.00% 3A vs 3B 0.847621 0.847874 0,03 % 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 % 2A vs 2B 0.916523 0.912872 0.40% 3a vs 3b 0.853842 0.847874 0.73% 私の実装における 0.00-0.03% の差は、浮動点のラウンドによるものであり、言語間の翻訳では避けられません。 The ssim.js differences come from algorithmic choices: no auto-downsampling, slightly different Gaussian approximation, and different boundary handling. None are bugs, but they compound into measurable error. 共通ピットフォール Porting MATLAB to JavaScript looks straightforward until it isn't. These are the issues that cost me time. Array Indexing MATLAB 配列は 1 から始まります。JavaScript 配列は 0 から始まります。誰もが知っています。 The bug is rarely obvious. You won't get an off-by-one error on the first pixel. You'll get slightly wrong values at image boundaries, where loops like translate to 近所のアクセス 成り , which hits index -1 when 0です。 for i = 1:height for (let i = 0; i < height; i++) img(i-1, j) img[(i-1) * width + j] i 修正: ループを書く前に紙に 3x3 サンプルを描きます. Check your boundary conditions explicitly. Column-Major vs Row-Major MATLAB stores matrices column-by-column. JavaScript TypedArrays store row-by-row. MATLAB の 3x3 マトリックス: [1 4 7] [2 5 8] → stored as [1, 2, 3, 4, 5, 6, 7, 8, 9] [3 6 9] JavaScriptで同じマトリックス: [1 4 7] [2 5 8] → stored as [1, 4, 7, 2, 5, 8, 3, 6, 9] [3 6 9] これは、インデックスを翻訳するときに重要です。MATLABの becomes JavaScriptでは、いや、 . img(row, col) img[row * width + col] img[col * height + row] ほとんどの画像ライブラリはすでにリードメジャーデータを送信していますので、大丈夫ですが、MATLAB マトリックス操作を文字通りにコピーしている場合は注意してください。 境界行動 MATLABの で、 そして、 have different default behaviors: conv2 filter2 imfilter conv2(A、K) — no padding, output shrinks conv2(A, K, 'same') — zero padding, output same size as input filter2(K、A、「有効」) — no padding, output shrinks — mirror padding at edges imfilter(A, K, 'symmetric') これを間違えて、あなたの結果は境界の近くのすべてのピクセルで異なります。11x11カーネルを持つ1000x1000画像の場合、それはあなたのピクセルの ~4%です。 SSIM 参照用 同 downsampling を用いて、その後 with メインコンピューティングのためのモード 詳細を見逃すと、なぜあなたの数字が2%の割引になるのか疑問に思うでしょう。 imfilter 'symmetric' filter2 'valid' Color Space Coefficients RGB をグレイスケールに変換することは単純なように見えます. Coefficients によって倍増し、チャンネルを合計します. But which coefficients? 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 Others use the simple average: Y = (R + G + B) / 3 違いはほとんどの画像では軽微です。しかし、MATLABに対して検証している場合は、正確なBT.601コエフェクティエントを使用してください。そうでなければ、本当にグレースケールの変換不一致であるファントムバグを追いかけます。 MATLABの暗示的な行動 MATLAB は、JavaScript が行わないことを自動的に行います。 : 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 関数には、その文書に埋め込まれたデフォルト値があります。 , , window size 11, sigma 1.5. miss one, and your implementation diverges. ウィンドウサイズ11、シグマ1.5を省略し、実装が異なります。 Default parameters K = [0.01, 0.03] L = 255 あなたのJavaScriptの最適化 正確さが先に現れますが、実装が MATLAB に匹敵すると、パフォーマンスが重要になります。科学的なアルゴリズムは数百万のピクセルを処理します。 これらの最適化により、SSIM の実装は ssim.js より 25-70% 速くなり、正確性を維持しました。 Use TypedArrays 通常のJavaScriptの配列は柔軟です. 彼らは混合型を保持し、動的に成長し、便利な方法を持っています. 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 には固定サイズ、固定タイプ、並列メモリがあります。JavaScript エンジンはそれらを最適化する方法を正確に知っています。 Use ほとんどのコンピュータで、 when you need extra precision (accumulating sums over large images). Use for final image output. Float32Array Float64Array Uint8ClampedArray 別々のフィルター NxN カーネルを搭載した 2D コンボレーションでは、ピクセルあたりの N2 倍数が必要です SSIM の 11x11 Gaussian の場合、これはピクセルあたり 121 回の操作です。 しかし、ガウシアンフィルターは分離可能です 2Dガウシアンは2Dガウシアンの外部製品です 1x11パスの代わりに、あなたは2つの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; } } This works for any separable kernel: Gaussian, box filter, or Sobel. Check if your kernel is separable before implementing 2D convolution. 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 } } SSIM では、すべてのバッファを前方に割り当てます:グレイスケール画像、平方画像、フィルター出力、および SSIM マップ。 Cache Computed Values Some values get reused. Don’t compute them twice. // 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 の Gaussian ウィンドウは常に 11x11 で σ=1.5 です。 計算にはマイクロ秒かかりますが、何千枚もの画像を処理していると、マイクロ秒が加算されます。 タイトル: Avoid Bounds Checking in 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 しかし、1000x1000画像の場合、あなたは何百万もの条件チェックを削除しています。 結果 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% ssim.js 86ms 0.05 0.73% ブレイク/SIM 64ms 0 0 0 0 0 0 0 25%速く、より正確で、賢いアルゴリズムのせいではなく、慎重なエンジニアリングのせいである。TypedArrays、分離可能なフィルター、バッファの再利用、キャッシュウィンドウ。 For the バージョン(コンボレーションの代わりにインテグラル画像を使用する)、ギャップはより広い:大画像のssim.jsよりも70%速い。 ヒッチハイカーのSSIM Takeaways Porting scientific algorithms from MATLAB to JavaScript is mechanical once you have the proper process. . Papers describe algorithms. MATLAB code implements them. These are not the same thing. Edge cases, default parameters, boundary handling - they're in the code, not the paper. Edge cases, default parameters, boundary handling - they're in the code, not the paper. Edge cases, default parameters, boundary handling - they're in the code, not the paper. file. Use the reference implementation .m . It's free, runs everywhere, and executes MATLAB code without modification. Set up ground-truth tests early. Run them in CI. When your numbers match to 4 decimal places, you're done. When they don't, you know immediately. 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 . Sometimes you won't match exactly. Boundary handling, floating-point precision, and intentional simplifications. That's fine. Know why you differ and by how much. Write it down. 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.