📷
画像処理100本ノックに挑戦|Prewittフィルタ(016/100)
これはなに?
画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の16本目です。
前回
実装
お題
Prewittフィルタ(3x3)を実装せよ。
Prewittフィルタはエッジ抽出フィルタの一種であり、次式で定義される。
(a)縦方向 (b)横方向
-1 -1 -1 -1 0 1
K = [ 0 0 0 ] K = [ -1 0 1 ]
1 1 1 -1 0 1
Coding
import sharp from 'sharp';
export async function prewittFilter(
inputPath: string,
outputVerticalPath: string,
outputHorizontalPath: 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 grayData = Buffer.alloc(width * height);
for (let i = 0; i < data.length; i += channels) {
const b = data[i];
const g = data[i + 1];
const r = data[i + 2];
const gray = Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b);
grayData[i / channels] = gray;
}
// パディングサイズを計算
const pad = Math.floor(kernelSize / 2);
// パディング付きの配列を作成
const paddedHeight = height + 2 * pad;
const paddedWidth = width + 2 * pad;
const paddedData = new Float32Array(paddedHeight * paddedWidth);
const tempData = new Float32Array(paddedHeight * paddedWidth);
// ゼロパディング
for (let y = 0; y < paddedHeight; y++) {
for (let x = 0; x < paddedWidth; x++) {
const destPos = y * paddedWidth + x;
if (y >= pad && y < paddedHeight - pad &&
x >= pad && x < paddedWidth - pad) {
const srcY = y - pad;
const srcX = x - pad;
const srcPos = srcY * width + srcX;
paddedData[destPos] = grayData[srcPos];
tempData[destPos] = grayData[srcPos];
}
}
}
// Prewittカーネルの定義
const verticalKernel = [
[-1, -1, -1],
[0, 0, 0],
[1, 1, 1]
];
const horizontalKernel = [
[-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1]
];
// 結果用の配列を作成
const verticalResult = Buffer.alloc(width * height);
const horizontalResult = Buffer.alloc(width * height);
// フィルタリング
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let sumVertical = 0;
let sumHorizontal = 0;
// カーネル領域の処理
for (let ky = 0; ky < kernelSize; ky++) {
for (let kx = 0; kx < kernelSize; kx++) {
const py = y + ky;
const px = x + kx;
const pixel = tempData[py * paddedWidth + px];
// 縦方向と横方向の畳み込み
sumVertical += pixel * verticalKernel[ky][kx];
sumHorizontal += pixel * horizontalKernel[ky][kx];
}
}
// 結果を保存(0-255の範囲にクリップ)
const pos = y * width + x;
verticalResult[pos] = Math.min(255, Math.max(0, sumVertical));
horizontalResult[pos] = Math.min(255, Math.max(0, sumHorizontal));
}
}
// 結果を保存(縦方向)
await sharp(verticalResult, {
raw: {
width,
height,
channels: 1
}
})
.toFile(outputVerticalPath);
// 結果を保存(横方向)
await sharp(horizontalResult, {
raw: {
width,
height,
channels: 1
}
})
.toFile(outputHorizontalPath);
console.log('Prewittフィルタ処理が完了しました');
} catch (error) {
console.error('画像処理中にエラーが発生しました:', error);
throw error;
}
}
Test
import { existsSync, unlinkSync } from 'fs';
import { join } from 'path';
import sharp from 'sharp';
import { prewittFilter } from './imageProcessor';
describe('Prewitt Filter Tests', () => {
const testInputPath = join(__dirname, '../test-images/test.jpeg');
const testOutputVerticalPath = join(__dirname, '../test-images/test-prewitt-v.jpg');
const testOutputHorizontalPath = join(__dirname, '../test-images/test-prewitt-h.jpg');
// 各テスト後に生成された画像を削除
afterEach(() => {
[testOutputVerticalPath, testOutputHorizontalPath].forEach(path => {
if (existsSync(path)) {
unlinkSync(path);
}
});
});
test('should successfully apply Prewitt filter', async () => {
await expect(prewittFilter(
testInputPath,
testOutputVerticalPath,
testOutputHorizontalPath
)).resolves.not.toThrow();
expect(existsSync(testOutputVerticalPath)).toBe(true);
expect(existsSync(testOutputHorizontalPath)).toBe(true);
});
test('should maintain image dimensions', async () => {
await prewittFilter(
testInputPath,
testOutputVerticalPath,
testOutputHorizontalPath
);
const inputMetadata = await sharp(testInputPath).metadata();
const verticalMetadata = await sharp(testOutputVerticalPath).metadata();
const horizontalMetadata = await sharp(testOutputHorizontalPath).metadata();
expect(verticalMetadata.width).toBe(inputMetadata.width);
expect(verticalMetadata.height).toBe(inputMetadata.height);
expect(horizontalMetadata.width).toBe(inputMetadata.width);
expect(horizontalMetadata.height).toBe(inputMetadata.height);
});
test('should detect edges', async () => {
await prewittFilter(
testInputPath,
testOutputVerticalPath,
testOutputHorizontalPath
);
const verticalImage = await sharp(testOutputVerticalPath)
.raw()
.toBuffer({ resolveWithObject: true });
const horizontalImage = await sharp(testOutputHorizontalPath)
.raw()
.toBuffer({ resolveWithObject: true });
let hasEdgesVertical = false;
let hasEdgesHorizontal = false;
let hasNonEdges = false;
// エッジと非エッジ領域の存在を確認
for (let i = 0; i < verticalImage.data.length; i++) {
const verticalValue = verticalImage.data[i];
const horizontalValue = horizontalImage.data[i];
if (verticalValue > 30) hasEdgesVertical = true;
if (horizontalValue > 30) hasEdgesHorizontal = true;
if (verticalValue < 10 && horizontalValue < 10) hasNonEdges = true;
if (hasEdgesVertical && hasEdgesHorizontal && hasNonEdges) break;
}
expect(hasEdgesVertical).toBe(true);
expect(hasEdgesHorizontal).toBe(true);
expect(hasNonEdges).toBe(true);
});
test('should have valid pixel values', async () => {
await prewittFilter(
testInputPath,
testOutputVerticalPath,
testOutputHorizontalPath
);
const verticalImage = await sharp(testOutputVerticalPath)
.raw()
.toBuffer({ resolveWithObject: true });
const horizontalImage = await sharp(testOutputHorizontalPath)
.raw()
.toBuffer({ resolveWithObject: true });
// すべてのピクセル値が有効な範囲(0-255)内にあることを確認
const isValidValue = (value: number) => value >= 0 && value <= 255;
expect(Array.from(verticalImage.data).every(isValidValue)).toBe(true);
expect(Array.from(horizontalImage.data).every(isValidValue)).toBe(true);
});
});
結果
入力 | 出力・縦方向 | 出力・横方向 |
---|---|---|
![]() |
![]() |
![]() |
Discussion