📷
画像処理100本ノックに挑戦|モーションフィルタ(012/100)
これはなに?
画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の12本目です。
前回
実装
お題
モーションフィルタ(3x3)を実装せよ。
モーションフィルタとは対角方向の平均値を取るフィルタであり、次式で定義される。
1/3 0 0
[ 0 1/3 0 ]
0 0 1/3
Coding
import sharp from 'sharp';
export async function motionFilter(
inputPath: string,
outputPath: string,
kernelSize: number = 3
): Promise<void> {
try {
const image = await sharp(inputPath)
.raw()
.toBuffer({ resolveWithObject: true });
const { data, info } = image;
const { width, height, channels } = info;
// カーネルを作成(対角行列)
const kernel = new Float32Array(kernelSize * kernelSize);
for (let i = 0; i < kernelSize; i++) {
kernel[i * kernelSize + i] = 1.0 / kernelSize; // 対角要素に1/kernelSizeを設定
}
// パディングサイズを計算
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 < paddedHeight; y++) {
for (let x = 0; x < paddedWidth; x++) {
for (let c = 0; c < channels; c++) {
const destPos = (y * paddedWidth + x) * channels + c;
if (y >= pad && y < paddedHeight - pad &&
x >= pad && x < paddedWidth - pad) {
const srcY = y - pad;
const srcX = x - pad;
const srcPos = (srcY * width + srcX) * channels + c;
temp[destPos] = data[srcPos];
} else {
temp[destPos] = 0;
}
}
}
}
// 結果用の配列を作成
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 kernelValue = kernel[ky * kernelSize + kx];
const pos = (py * paddedWidth + px) * channels + c;
sum += temp[pos] * kernelValue;
}
}
// 結果を保存
const destPos = (y * width + x) * channels + c;
result[destPos] = 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 { motionFilter } from './imageProcessor';
describe('Motion Filter Tests', () => {
const testInputPath = join(__dirname, '../test-images/test.jpeg');
const testOutputPath = join(__dirname, '../test-images/test-motion.png');
afterEach(() => {
if (existsSync(testOutputPath)) {
unlinkSync(testOutputPath);
}
});
test('should successfully apply motion filter', async () => {
await expect(motionFilter(testInputPath, testOutputPath))
.resolves.not.toThrow();
expect(existsSync(testOutputPath)).toBe(true);
});
test('should maintain image dimensions', async () => {
await motionFilter(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 apply diagonal blur', async () => {
const kernelSize = 3;
await motionFilter(testInputPath, testOutputPath, kernelSize);
const outputImage = await sharp(testOutputPath)
.raw()
.toBuffer({ resolveWithObject: true });
// カーネルサイズ分の領域をチェック
const { data, info } = outputImage;
const { channels } = info;
// ピクセル値が有効な範囲内にあることを確認
for (let i = 0; i < data.length; i += channels) {
for (let c = 0; c < channels; c++) {
expect(data[i + c]).toBeGreaterThanOrEqual(0);
expect(data[i + c]).toBeLessThanOrEqual(255);
}
}
});
});
結果
入力 | 結果 |
---|---|
![]() |
![]() |
Discussion