📷
画像処理100本ノックに挑戦|Embossフィルタ(018/100)
これはなに?
画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の18本目です。
前回
実装
お題
Embossフィルタを実装せよ。
Embossフィルタとは輪郭部分を浮き出しにするフィルタで、次式で定義される。
-2 -1 0
K = [ -1 1 1 ]
0 1 2
Coding
import sharp from 'sharp';
export async function embossFilter(
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 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];
}
}
}
// Embossカーネルの定義
const kernel = [
[-2, -1, 0],
[-1, 1, 1],
[0, 1, 2]
];
// 結果用の配列を作成
const result = Buffer.alloc(width * height);
// フィルタリング
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++) {
const py = y + ky;
const px = x + kx;
const pixel = tempData[py * paddedWidth + px];
sum += pixel * kernel[ky][kx];
}
}
// 結果を保存(0-255の範囲にクリップ)
const pos = y * width + x;
result[pos] = Math.min(255, Math.max(0, sum));
}
}
// 結果を保存
await sharp(result, {
raw: {
width,
height,
channels: 1
}
})
.toFile(outputPath);
console.log('Embossフィルタ処理が完了しました');
} catch (error) {
console.error('画像処理中にエラーが発生しました:', error);
throw error;
}
}
Test
import { existsSync, unlinkSync } from 'fs';
import { join } from 'path';
import sharp from 'sharp';
import { embossFilter } from './imageProcessor';
describe('Emboss Filter Tests', () => {
const testInputPath = join(__dirname, '../test-images/test.jpeg');
const testOutputPath = join(__dirname, '../test-images/test-emboss.jpg');
afterEach(() => {
if (existsSync(testOutputPath)) {
unlinkSync(testOutputPath);
}
});
test('should successfully apply emboss filter', async () => {
await expect(embossFilter(testInputPath, testOutputPath))
.resolves.not.toThrow();
expect(existsSync(testOutputPath)).toBe(true);
});
test('should maintain image dimensions', async () => {
await embossFilter(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 create emboss effect', async () => {
await embossFilter(testInputPath, testOutputPath);
const outputImage = await sharp(testOutputPath)
.raw()
.toBuffer({ resolveWithObject: true });
// エンボス効果の特徴を確認(明るい部分と暗い部分の両方が存在すること)
let hasHighValues = false;
let hasLowValues = false;
let hasMidValues = false;
for (let i = 0; i < outputImage.data.length; i++) {
const value = outputImage.data[i];
if (value > 200) hasHighValues = true;
if (value < 50) hasLowValues = true;
if (value > 100 && value < 150) hasMidValues = true;
if (hasHighValues && hasLowValues && hasMidValues) break;
}
expect(hasHighValues).toBe(true);
expect(hasLowValues).toBe(true);
expect(hasMidValues).toBe(true);
});
test('should have valid pixel values', async () => {
await embossFilter(testInputPath, testOutputPath);
const outputImage = await sharp(testOutputPath)
.raw()
.toBuffer({ resolveWithObject: true });
// すべてのピクセル値が有効な範囲内にあることを確認
const isValidValue = (value: number) => value >= 0 && value <= 255;
expect(Array.from(outputImage.data).every(isValidValue)).toBe(true);
});
});
結果
入力 | 出力 |
---|---|
![]() |
![]() |
Discussion