📷
画像処理100本ノックに挑戦|ガウシアンフィルタ(009/100)
これはなに?
画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の9本目です。
前回
実装
お題
ガウシアンフィルタ(3x3、標準偏差1.3)を実装し、imori_noise.jpgのノイズを除去せよ。
ガウシアンフィルタとは画像の平滑化(滑らかにする)を行うフィルタの一種であり、ノイズ除去にも使われる。
ノイズ除去には他にも、メディアンフィルタ(Q.10)、平滑化フィルタ(Q.11)、LoGフィルタ(Q.19)などがある。
ガウシアンフィルタは注目画素の周辺画素を、ガウス分布による重み付けで平滑化し、次式で定義される。 このような重みはカーネルやフィルタと呼ばれる。
ただし、画像の端はこのままではフィルタリングできないため、画素が足りない部分は0で埋める。これを0パディングと呼ぶ。 かつ、重みは正規化する。(sum g = 1)
重みはガウス分布から次式になる。
重み g(x,y,s) = 1/ (2 * pi * sigma * sigma) * exp( - (x^2 + y^2) / (2*s^2))
標準偏差s = 1.3による8近傍ガウシアンフィルタは
1 2 1
K = 1/16 [ 2 4 2 ]
1 2 1
Coding
import sharp from 'sharp';
export async function gaussianFilter(
inputPath: string,
outputPath: string,
kernelSize: number = 3,
sigma: number = 1.3
): Promise<void> {
try {
const image = await sharp(inputPath)
.raw()
.toBuffer({ resolveWithObject: true });
const { data, info } = image;
const { width, height, channels } = info;
// パディングサイズを計算
const pad = Math.floor(kernelSize / 2);
// パディング付きの配列を作成
const paddedHeight = height + 2 * pad;
const paddedWidth = width + 2 * pad;
const temp = new Float32Array(paddedHeight * paddedWidth * channels);
// ゼロパディング
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
for (let c = 0; c < channels; c++) {
const srcPos = (y * width + x) * channels + c;
const destPos = ((y + pad) * paddedWidth + (x + pad)) * channels + c;
temp[destPos] = data[srcPos];
}
}
}
// カーネルを準備(3x3の場合、1/16で正規化された重み付け)
const kernel = new Float32Array(kernelSize * kernelSize);
if (kernelSize === 3) {
// 3x3カーネルの場合、お手本の値を直接使用
kernel.set([
1/16, 2/16, 1/16,
2/16, 4/16, 2/16,
1/16, 2/16, 1/16
]);
} else {
// その他のサイズの場合は計算
let sum = 0;
for (let y = -pad; y <= pad; y++) {
for (let x = -pad; x <= pad; x++) {
const exp = -(x * x + y * y) / (2 * sigma * sigma);
const value = Math.exp(exp);
kernel[(y + pad) * kernelSize + (x + pad)] = value;
sum += value;
}
}
// カーネルの正規化
for (let i = 0; i < kernel.length; i++) {
kernel[i] /= sum;
}
}
// 結果用の配列を作成
const result = Buffer.alloc(width * height * channels);
// フィルタリング
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
for (let c = 0; c < channels; c++) {
let sum = 0;
// カーネルとの畳み込み
for (let ky = 0; ky < kernelSize; ky++) {
for (let kx = 0; kx < kernelSize; kx++) {
const py = y + ky;
const px = x + kx;
const sourcePos = (py * paddedWidth + px) * channels + c;
sum += temp[sourcePos] * kernel[ky * kernelSize + kx];
}
}
// 結果を保存
const destPos = (y * width + x) * channels + c;
result[destPos] = Math.min(255, Math.max(0, Math.round(sum)));
}
}
}
// 結果を保存
await sharp(result, {
raw: {
width,
height,
channels
}
})
.toFile(outputPath);
} catch (error) {
console.error('ガウシアンフィルタの適用中にエラーが発生しました:', error);
throw error;
}
}
Test
import { existsSync, unlinkSync } from 'fs';
import { join } from 'path';
import sharp from 'sharp';
import { gaussianFilter } from './imageProcessor';
describe('Gaussian Filter Tests', () => {
const testInputPath = join(__dirname, '../test-images/test_noise.jpg');
const testOutputPath = join(__dirname, '../test-images/test-gaussian.png');
afterEach(() => {
if (existsSync(testOutputPath)) {
unlinkSync(testOutputPath);
}
});
test('should successfully apply gaussian filter', async () => {
await expect(gaussianFilter(testInputPath, testOutputPath))
.resolves.not.toThrow();
expect(existsSync(testOutputPath)).toBe(true);
});
test('should maintain image dimensions', async () => {
await gaussianFilter(testInputPath, testOutputPath);
const inputMetadata = await sharp(testInputPath).metadata();
const outputMetadata = await sharp(testOutputPath).metadata();
expect(outputMetadata.width).toBe(inputMetadata.width);
expect(outputMetadata.height).toBe(inputMetadata.height);
});
test('should reduce noise', async () => {
await gaussianFilter(testInputPath, testOutputPath);
const inputImage = await sharp(testInputPath)
.raw()
.toBuffer({ resolveWithObject: true });
const outputImage = await sharp(testOutputPath)
.raw()
.toBuffer({ resolveWithObject: true });
// ノイズ低減を確認(標準偏差の比較)
const calculateStdDev = (data: Buffer) => {
const mean = data.reduce((sum, val) => sum + val, 0) / data.length;
const variance = data.reduce((sum, val) => sum + (val - mean) ** 2, 0) / data.length;
return Math.sqrt(variance);
};
const inputStdDev = calculateStdDev(inputImage.data);
const outputStdDev = calculateStdDev(outputImage.data);
// フィルタ適用後の標準偏差が小さくなっているはず
expect(outputStdDev).toBeLessThan(inputStdDev);
});
});
結果
入力 | 結果 |
---|---|
Discussion