📷
画像処理100本ノックに挑戦|大津の二値化(004/100)
これはなに?
画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の4本目です。
前回
実装
お題
大津の二値化を実装せよ。 大津の二値化とは判別分析法と呼ばれ、二値化における分離の閾値を自動決定する手法である。 これはクラス内分散とクラス間分散の比から計算される。
Coding
import sharp from 'sharp';
export async function otsuBinarization(inputPath: string, outputPath: string): Promise<{ threshold: number }> {
try {
// まず画像をグレースケールに変換してピクセルデータを取得
const image = await sharp(inputPath)
.grayscale()
.raw()
.toBuffer({ resolveWithObject: true });
const { data } = image;
// ヒストグラムの作成(0-255の出現頻度)
const histogram = new Array(256).fill(0);
for (const pixel of data) {
histogram[pixel]++;
}
// 総ピクセル数
const totalPixels = data.length;
let maxVariance = 0;
let optimalThreshold = 0;
// すべての可能な閾値について分離度を計算
for (let threshold = 0; threshold < 256; threshold++) {
// クラス1(閾値未満)の画素数と平均
let w1 = 0;
let sum1 = 0;
for (let i = 0; i < threshold; i++) {
w1 += histogram[i];
sum1 += i * histogram[i];
}
// クラス2(閾値以上)の画素数と平均
let w2 = totalPixels - w1;
if (w1 === 0 || w2 === 0) continue;
// クラス1の平均
const mean1 = sum1 / w1;
// 全体の平均を計算
let totalSum = 0;
for (let i = 0; i < 256; i++) {
totalSum += i * histogram[i];
}
const meanTotal = totalSum / totalPixels;
// クラス間分散を計算
const variance = Math.pow(meanTotal * w1 - sum1, 2) / (w1 * w2);
// より良い閾値が見つかった場合は更新
if (variance > maxVariance) {
maxVariance = variance;
optimalThreshold = threshold;
}
}
// 計算された閾値で二値化を実行
await sharp(inputPath)
.grayscale()
.threshold(optimalThreshold)
.toFile(outputPath);
console.log(`大津の二値化が完了しました(閾値: ${optimalThreshold})`);
return { threshold: optimalThreshold };
} catch (error) {
console.error('画像処理中にエラーが発生しました:', error);
throw error;
}
}
Test
import { existsSync, unlinkSync } from 'fs';
import { join } from 'path';
import sharp from 'sharp';
import { otsuBinarization } from './imageProcessor';
describe('Otsu Binarization Tests', () => {
const testInputPath = join(__dirname, '../test-images/test.jpeg');
const testOutputPath = join(__dirname, '../test-images/test-otsu.png');
afterEach(() => {
if (existsSync(testOutputPath)) {
unlinkSync(testOutputPath);
}
});
test('should successfully apply Otsu binarization', async () => {
const result = await otsuBinarization(testInputPath, testOutputPath);
expect(existsSync(testOutputPath)).toBe(true);
expect(result.threshold).toBeGreaterThanOrEqual(0);
expect(result.threshold).toBeLessThanOrEqual(255);
});
test('should output binary image with only black and white pixels', async () => {
await otsuBinarization(testInputPath, testOutputPath);
const outputImage = await sharp(testOutputPath)
.raw()
.toBuffer({ resolveWithObject: true });
const uniqueValues = new Set(outputImage.data);
expect(uniqueValues.size).toBeLessThanOrEqual(2);
expect(Array.from(uniqueValues).every(v => v === 0 || v === 255)).toBe(true);
});
test('should maintain image dimensions', async () => {
await otsuBinarization(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 calculate reasonable threshold value', async () => {
const result = await otsuBinarization(testInputPath, testOutputPath);
// 閾値が妥当な範囲内にあることを確認
expect(result.threshold).toBeGreaterThan(0);
expect(result.threshold).toBeLessThan(255);
});
});
結果
入力 | 結果 |
---|---|
Discussion