📷
画像処理100本ノックに挑戦|Sobelフィルタ(015/100)
これはなに?
画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の15本目です。
前回
実装
お題
Sobelフィルタ(3x3)を実装せよ。
ソーベルフィルタ(Sobelフィルタ)は特定方向(縦や横)のエッジのみを抽出するフィルタであり、次式でそれぞれ定義される。
(a)縦方向 (b)横方向
1 2 1 1 0 -1
K = [ 0 0 0 ] K = [ 2 0 -2 ]
-1 -2 -1 1 0 -1
Coding
import sharp from 'sharp';
export async function sobelFilter(
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];
}
}
}
// Sobelカーネルの定義
const verticalKernel = [
[1, 2, 1],
[0, 0, 0],
[-1, -2, -1]
];
const horizontalKernel = [
[1, 0, -1],
[2, 0, -2],
[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, Math.abs(sumVertical)));
horizontalResult[pos] = Math.min(255, Math.max(0, Math.abs(sumHorizontal)));
}
}
// 結果を保存(縦方向)
await sharp(verticalResult, {
raw: {
width,
height,
channels: 1
}
})
.toFile(outputVerticalPath);
// 結果を保存(横方向)
await sharp(horizontalResult, {
raw: {
width,
height,
channels: 1
}
})
.toFile(outputHorizontalPath);
console.log('Sobelフィルタ処理が完了しました');
} catch (error) {
console.error('画像処理中にエラーが発生しました:', error);
throw error;
}
}
Test
import { existsSync, unlinkSync } from 'fs';
import { join } from 'path';
import sharp from 'sharp';
import { sobelFilter } from './imageProcessor';
describe('Sobel Filter Tests', () => {
const testInputPath = join(__dirname, '../test-images/test.jpeg');
const testOutputVerticalPath = join(__dirname, '../test-images/test-sobel-v.jpg');
const testOutputHorizontalPath = join(__dirname, '../test-images/test-sobel-h.jpg');
afterEach(() => {
[testOutputVerticalPath, testOutputHorizontalPath].forEach(path => {
if (existsSync(path)) {
unlinkSync(path);
}
});
});
test('should successfully apply Sobel filter', async () => {
await expect(sobelFilter(
testInputPath,
testOutputVerticalPath,
testOutputHorizontalPath
)).resolves.not.toThrow();
expect(existsSync(testOutputVerticalPath)).toBe(true);
expect(existsSync(testOutputHorizontalPath)).toBe(true);
});
test('should maintain image dimensions', async () => {
await sobelFilter(
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 with correct intensity', async () => {
await sobelFilter(
testInputPath,
testOutputVerticalPath,
testOutputHorizontalPath
);
const verticalImage = await sharp(testOutputVerticalPath)
.raw()
.toBuffer({ resolveWithObject: true });
const horizontalImage = await sharp(testOutputHorizontalPath)
.raw()
.toBuffer({ resolveWithObject: true });
// エッジ検出の強度を確認
let hasStrongEdges = false;
for (let i = 0; i < verticalImage.data.length; i++) {
if (verticalImage.data[i] > 100 || horizontalImage.data[i] > 100) {
hasStrongEdges = true;
break;
}
}
expect(hasStrongEdges).toBe(true);
});
});
結果
入力 | 出力・縦方向 | 出力・横方向 |
---|---|---|
![]() |
![]() |
![]() |
Discussion