📷
画像処理100本ノックに挑戦|アフィン変換(スキュー)(031/100)
これはなに?
画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の31本目です。
前回
実装
お題
(1)アフィン変換を用いて、出力(1)のようなX-sharing(dx = 30)画像を作成せよ。
(2)アフィン変換を用いて、出力2のようなY-sharing(dy = 30)画像を作成せよ。
(3)アフィン変換を用いて、出力3のような幾何変換した(dx = 30, dy = 30)画像を作成せよ。
このような画像はスキュー画像と呼ばれ、画像を斜め方向に伸ばした画像である。
出力(1)の場合、x方向にdxだけ引き伸ばした画像はX-sharingと呼ばれる。
出力(2)の場合、y方向にdyだけ引き伸ばした画像はY-sharingと呼ばれる。
それぞれ次式のアフィン変換で実現できる。 ただし、元画像のサイズがh x wとする。
(1) X-sharing (2) Y-sharing
a = dx / h a = dy / w
x' 1 a tx x x' 1 0 tx x
[ y' ] = [ 0 1 ty ][ y ] [ y' ] = [ a 1 ty ][ y ]
1 0 0 1 1 1 0 0 1 1
Coding
imageProcessor.ts
import sharp from 'sharp';
interface SkewParams {
dx: number; // X方向のスキュー量
dy: number; // Y方向のスキュー量
}
export async function skewTransform(
inputPath: string,
outputPath: string,
params: SkewParams
): Promise<void> {
try {
// 画像を読み込む
const image = await sharp(inputPath)
.raw()
.toBuffer({ resolveWithObject: true });
const { data, info } = image;
const { width: W, height: H, channels } = info;
// アフィン変換のパラメータを計算
const a = 1; // スケールX
const b = params.dx / H; // X-sharing
const c = params.dy / W; // Y-sharing
const d = 1; // スケールY
const tx = 0;
const ty = 0;
// 新しい画像サイズを計算
const H_new = Math.ceil(params.dy + H);
const W_new = Math.ceil(params.dx + W);
// 新しいバッファーを作成(黒で初期化)
const newBuf = Buffer.alloc(W_new * H_new * channels);
// アフィン変換の逆行列の係数を計算
const adbc = a * d - b * c;
if (Math.abs(adbc) < 1e-10) {
throw new Error('アフィン変換行列が特異です');
}
// 一時的な画像バッファー(パディング付き)
const tempBuf = Buffer.alloc((W + 2) * (H + 2) * channels);
// 元画像を一時バッファーの中心にコピー
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
for (let c = 0; c < channels; c++) {
const srcPos = (y * W + x) * channels + c;
const destPos = ((y + 1) * (W + 2) + (x + 1)) * channels + c;
tempBuf[destPos] = data[srcPos];
}
}
}
// 各ピクセルに対して変換を適用
for (let y_new = 0; y_new < H_new; y_new++) {
for (let x_new = 0; x_new < W_new; x_new++) {
// 逆変換で元の座標を求める
const x = Math.round((d * x_new - b * y_new) / adbc) + 1;
const y = Math.round((-c * x_new + a * y_new) / adbc) + 1;
// 範囲内に収める
const x_clipped = Math.min(Math.max(x, 0), W + 1);
const y_clipped = Math.min(Math.max(y, 0), H + 1);
// ピクセルデータをコピー
for (let c = 0; c < channels; c++) {
const srcPos = (y_clipped * (W + 2) + x_clipped) * channels + c;
const destPos = (y_new * W_new + x_new) * channels + c;
newBuf[destPos] = tempBuf[srcPos];
}
}
}
// 新しい画像を作成して保存
await sharp(newBuf, {
raw: {
width: W_new,
height: H_new,
channels
}
})
.toFile(outputPath);
console.log('スキュー変換が完了しました');
} catch (error) {
console.error('スキュー変換中にエラーが発生しました:', error);
throw error;
}
}
index.ts
import * as path from 'path';
import { affineTransform } from './imageProcessor';
async function main() {
// __dirnameを使用して相対パスを解決
const outputPath1 = path.join(__dirname, '../output_x_sharing.jpg');
const outputPath2 = path.join(__dirname, '../output_y_sharing.jpg');
const outputPath3 = path.join(__dirname, '../output_xy_sharing.jpg');
try {
console.log('画像処理を開始します...');
// Case 1: X-sharing (dx = 30)
await skewTransform(inputPath, outputPath1, {
dx: 30,
dy: 0
});
console.log('X-sharing の処理が完了しました');
// Case 2: Y-sharing (dy = 30)
await skewTransform(inputPath, outputPath2, {
dx: 0,
dy: 30
});
console.log('Y-sharing の処理が完了しました');
// Case 3: X-Y sharing (dx = 30, dy = 30)
await skewTransform(inputPath, outputPath3, {
dx: 30,
dy: 30
});
console.log('X-Y sharing の処理が完了しました');
} catch (error) {
console.error('プログラムの実行に失敗しました:', error);
}
}
main();
結果
入力 | 出力(1) | 出力(2) | 出力(3) |
---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Discussion