📷
画像処理100本ノックに挑戦|減色処理(006/100)
これはなに?
画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の6本目です。
前回
実装
お題
ここでは画像の値を256^3から4^3、すなわちR,G,B in {32, 96, 160, 224}の各4値に減色せよ。 これは量子化操作である。 各値に関して、以下の様に定義する。
val = { 32 ( 0 <= val < 64)
96 ( 64 <= val < 128)
160 (128 <= val < 192)
224 (192 <= val < 256)
Coding
愚直に実装すると以下のような条件分岐になるかと思います。
Math.floor(value / 64) * 64 + 32;
の一つの数式で表現できるようになり、シンプルでパフォーマンスも高いコードが書けます。
export function quantizeValue(value: number): number {
if (value < 64) return 32;
if (value < 128) return 96;
if (value < 192) return 160;
return 224;
}
import sharp from 'sharp';
export function quantizeValue(value: number): number {
// floor(value / 64) * 64 + 32 の実装
return Math.floor(value / 64) * 64 + 32;
}
export async function quantizeColors(inputPath: string, outputPath: string): Promise<void> {
try {
// 画像を読み込む
const image = await sharp(inputPath)
.raw()
.toBuffer({ resolveWithObject: true });
const { data, info } = image;
const { width, height, channels } = info;
// 新しい画像データ用のバッファを作成
const newData = Buffer.alloc(data.length);
// 各ピクセルの各チャネルを量子化
for (let i = 0; i < data.length; i += channels) {
// RGB各チャネルを量子化(C++のコードと同じロジック)
newData[i] = quantizeValue(data[i]); // R
newData[i + 1] = quantizeValue(data[i + 1]); // G
newData[i + 2] = quantizeValue(data[i + 2]); // B
// アルファチャネルがある場合はそのままコピー
if (channels === 4) {
newData[i + 3] = data[i + 3];
}
}
// 画像を保存
await sharp(newData, {
raw: {
width,
height,
channels
}
})
.png({
quality: 100,
compressionLevel: 9
})
.toFile(outputPath);
console.log('減色処理が完了しました');
} catch (error) {
console.error('画像処理中にエラーが発生しました:', error);
throw error;
}
}
Test
import { existsSync, unlinkSync } from 'fs';
import { join } from 'path';
import sharp from 'sharp';
import { quantizeColors, quantizeValue } from './imageProcessor';
describe('Color Quantization Tests', () => {
const testInputPath = join(__dirname, '../test-images/test.jpeg');
const testOutputPath = join(__dirname, '../test-images/test-quantized.png');
afterEach(() => {
if (existsSync(testOutputPath)) {
unlinkSync(testOutputPath);
}
});
test('should quantize values using floor division algorithm', () => {
const testCases = [
{ input: 0, expected: 32 }, // 0/64 = 0 -> 0*64 + 32 = 32
{ input: 63, expected: 32 }, // 63/64 = 0 -> 0*64 + 32 = 32
{ input: 64, expected: 96 }, // 64/64 = 1 -> 1*64 + 32 = 96
{ input: 127, expected: 96 }, // 127/64 = 1 -> 1*64 + 32 = 96
{ input: 128, expected: 160 }, // 128/64 = 2 -> 2*64 + 32 = 160
{ input: 191, expected: 160 }, // 191/64 = 2 -> 2*64 + 32 = 160
{ input: 192, expected: 224 }, // 192/64 = 3 -> 3*64 + 32 = 224
{ input: 255, expected: 224 } // 255/64 = 3 -> 3*64 + 32 = 224
];
testCases.forEach(({ input, expected }) => {
const result = quantizeValue(input);
expect(result).toBe(expected);
});
});
test('should successfully process image', async () => {
await expect(quantizeColors(testInputPath, testOutputPath))
.resolves.not.toThrow();
expect(existsSync(testOutputPath)).toBe(true);
});
test('should maintain image dimensions', async () => {
await quantizeColors(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 only output specific quantized values', async () => {
await quantizeColors(testInputPath, testOutputPath);
const outputImage = await sharp(testOutputPath)
.raw()
.toBuffer({ resolveWithObject: true });
const expectedValues = new Set([32, 96, 160, 224]);
const channels = outputImage.info.channels;
// サンプルピクセルをチェック
for (let i = 0; i < Math.min(1000, outputImage.data.length); i += channels) {
for (let c = 0; c < 3; c++) { // RGB各チャネルをチェック
const value = outputImage.data[i + c];
if (!expectedValues.has(value)) {
console.log(`Unexpected value: ${value} at position ${i + c}`);
}
expect(expectedValues.has(value)).toBe(true);
}
}
});
});
結果
入力 | 結果 |
---|---|
Discussion