📷
画像処理100本ノックに挑戦|HSV変換(005/100)
これはなに?
画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の5本目です。
前回
実装
お題
HSV変換を実装して、色相Hを反転せよ。
Coding
import sharp from 'sharp';
interface HSV {
h: number; // 0-360
s: number; // 0-1
v: number; // 0-1
}
export function rgbToHsv(rOrRgb: number | [number, number, number], g?: number, b?: number): HSV {
let r: number, gb: number, bb: number;
if (Array.isArray(rOrRgb)) {
[r, gb, bb] = rOrRgb;
} else {
r = rOrRgb;
gb = g!;
bb = b!;
}
// 以下、既存の実装
const red = r / 255;
const green = gb / 255;
const blue = bb / 255;
const max = Math.max(red, green, blue);
const min = Math.min(red, green, blue);
const diff = max - min;
let h = 0;
const s = max === 0 ? 0 : diff / max;
const v = max;
if (max !== min) {
if (max === red) {
h = 60 * ((green - blue) / diff) + (green < blue ? 360 : 0);
} else if (max === green) {
h = 60 * ((blue - red) / diff) + 120;
} else if (max === blue) {
h = 60 * ((red - green) / diff) + 240;
}
}
return { h, s, v };
}
export function hsvToRgb(hsv: HSV): [number, number, number] {
const { h, s, v } = hsv;
const c = v * s;
const hPrime = h / 60;
const x = c * (1 - Math.abs((hPrime % 2) - 1));
let r = 0, g = 0, b = 0;
if (0 <= hPrime && hPrime < 1) { [r, g, b] = [c, x, 0]; }
else if (1 <= hPrime && hPrime < 2) { [r, g, b] = [x, c, 0]; }
else if (2 <= hPrime && hPrime < 3) { [r, g, b] = [0, c, x]; }
else if (3 <= hPrime && hPrime < 4) { [r, g, b] = [0, x, c]; }
else if (4 <= hPrime && hPrime < 5) { [r, g, b] = [x, 0, c]; }
else if (5 <= hPrime && hPrime < 6) { [r, g, b] = [c, 0, x]; }
const m = v - c;
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255)
];
}
export async function invertHue(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) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const hsv = rgbToHsv(r, g, b);
hsv.h = (hsv.h + 180) % 360;
const [newR, newG, newB] = hsvToRgb(hsv);
newData[i] = newR;
newData[i + 1] = newG;
newData[i + 2] = newB;
if (channels === 4) {
newData[i + 3] = data[i + 3];
}
}
await sharp(newData, {
raw: {
width,
height,
channels
}
})
.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 { invertHue, rgbToHsv, hsvToRgb } from './imageProcessor';
describe('HSV Color Conversion Tests', () => {
test('should convert pure red RGB to HSV', () => {
const hsv = rgbToHsv(255, 0, 0);
expect(hsv.h).toBeCloseTo(0);
expect(hsv.s).toBeCloseTo(1);
expect(hsv.v).toBeCloseTo(1);
});
test('should convert pure green RGB to HSV', () => {
const hsv = rgbToHsv(0, 255, 0);
expect(hsv.h).toBeCloseTo(120);
expect(hsv.s).toBeCloseTo(1);
expect(hsv.v).toBeCloseTo(1);
});
test('should convert pure blue RGB to HSV', () => {
const hsv = rgbToHsv(0, 0, 255);
expect(hsv.h).toBeCloseTo(240);
expect(hsv.s).toBeCloseTo(1);
expect(hsv.v).toBeCloseTo(1);
});
test('should convert HSV back to RGB correctly', () => {
const originalRgb: [number, number, number] = [255, 0, 0];
const hsv = rgbToHsv(originalRgb); // 配列として渡すことができる
const [r, g, b] = hsvToRgb(hsv);
expect(r).toBeCloseTo(255);
expect(g).toBeCloseTo(0);
expect(b).toBeCloseTo(0);
});
test('should handle black correctly', () => {
const hsv = rgbToHsv(0, 0, 0);
expect(hsv.h).toBe(0);
expect(hsv.s).toBe(0);
expect(hsv.v).toBe(0);
});
test('should handle white correctly', () => {
const hsv = rgbToHsv(255, 255, 255);
expect(hsv.h).toBe(0);
expect(hsv.s).toBe(0);
expect(hsv.v).toBe(1);
});
});
// 画像処理の統合テスト
describe('Image Processing Tests', () => {
const testInputPath = join(__dirname, '../test-images/test.jpeg');
const testOutputPath = join(__dirname, '../test-images/test-inverted.jpg');
afterEach(() => {
if (existsSync(testOutputPath)) {
unlinkSync(testOutputPath);
}
});
test('should successfully process image', async () => {
await expect(invertHue(testInputPath, testOutputPath))
.resolves.not.toThrow();
expect(existsSync(testOutputPath)).toBe(true);
});
test('should handle non-existent input file', async () => {
const nonExistentPath = 'non-existent.jpg';
await expect(invertHue(nonExistentPath, testOutputPath))
.rejects
.toThrow();
});
});
結果
入力 | 結果 |
---|---|
Discussion