📷
画像処理100本ノックに挑戦|アフィン変換(回転)(030/100)
これはなに?
画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の30本目です。
実装
お題
(1)アフィン変換を用いて、反時計方向に30度回転させよ。
(2) アフィン変換を用いて、反時計方向に30度回転した画像で中心座標を固定することで、なるべく黒い領域がなくなるように画像を作成せよ。 (ただし、単純なアフィン変換を行うと画像が切れてしまうので、工夫を要する。)
Coding
imageProcessor.ts
import sharp from 'sharp';
interface AffineParams {
a: number; // scale x
b: number; // shear x
c: number; // shear y
d: number; // scale y
tx: number; // translate x
ty: number; // translate y
adjustCenter?: boolean; // 中心座標を調整するかどうか
}
export async function affineTransform(
inputPath: string,
outputPath: string,
params: AffineParams
): Promise<void> {
try {
// 画像を読み込む
const image = await sharp(inputPath)
.raw()
.toBuffer({ resolveWithObject: true });
const { data, info } = image;
const { width, height, channels } = info;
// アフィン変換後の新しいサイズを計算
const newWidth = Math.round(width);
const newHeight = Math.round(height);
// 新しいバッファーを作成(黒で初期化)
const newBuf = Buffer.alloc(newWidth * newHeight * channels);
// アフィン変換の逆行列の係数を計算
const adbc = params.a * params.d - params.b * params.c;
if (Math.abs(adbc) < 1e-10) {
throw new Error('アフィン変換行列が特異です');
}
// 中心座標の計算用の配列
let xCoords: number[] = [];
let yCoords: number[] = [];
// まず全座標を計算して中心調整のオフセットを求める
for (let y = 0; y < newHeight; y++) {
for (let x = 0; x < newWidth; x++) {
// 逆変換で元の座標を求める
const srcX = Math.round(
(params.d * x - params.b * y) / adbc - params.tx
);
const srcY = Math.round(
(-params.c * x + params.a * y) / adbc - params.ty
);
xCoords.push(srcX);
yCoords.push(srcY);
}
}
// 中心座標の調整値を計算
let dcx = 0;
let dcy = 0;
if (params.adjustCenter) {
dcx = Math.round((Math.max(...xCoords) + Math.min(...xCoords)) / 2 - width / 2);
dcy = Math.round((Math.max(...yCoords) + Math.min(...yCoords)) / 2 - height / 2);
}
// 各ピクセルに対して変換を適用
for (let y = 0; y < newHeight; y++) {
for (let x = 0; x < newWidth; x++) {
// 逆変換で元の座標を求める
const srcX = Math.round(
(params.d * x - params.b * y) / adbc - params.tx
) - dcx;
const srcY = Math.round(
(-params.c * x + params.a * y) / adbc - params.ty
) - dcy;
// 元画像の範囲内かチェック
if (
srcX >= 0 && srcX < width &&
srcY >= 0 && srcY < height
) {
// 元のピクセル位置と新しいピクセル位置を計算
const srcPos = (srcY * width + srcX) * channels;
const destPos = (y * newWidth + x) * channels;
// ピクセルデータをコピー
for (let c = 0; c < channels; c++) {
newBuf[destPos + c] = data[srcPos + c];
}
}
}
}
// 新しい画像を作成して保存
await sharp(newBuf, {
raw: {
width: newWidth,
height: newHeight,
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 inputPath = path.join(__dirname, '../imori.jpg');
const outputPath1 = path.join(__dirname, '../output_scale.jpg');
const outputPath2 = path.join(__dirname, '../output_scale_translate.jpg');
try {
console.log('画像処理を開始します...');
// 回転角度(度数法)
const angle = 30;
// ラジアンに変換
const theta = -(angle * Math.PI) / 180;
// Case 1: 単純な回転
await affineTransform(inputPath, outputPath1, {
a: Math.cos(theta), // 回転行列の要素
b: -Math.sin(theta), // 回転行列の要素
c: Math.sin(theta), // 回転行列の要素
d: Math.cos(theta), // 回転行列の要素
tx: 0,
ty: 0,
adjustCenter: false
});
console.log('単純回転の処理が完了しました');
// Case 2: 中心座標を固定した回転
await affineTransform(inputPath, outputPath2, {
a: Math.cos(theta),
b: -Math.sin(theta),
c: Math.sin(theta),
d: Math.cos(theta),
tx: 0,
ty: 0,
adjustCenter: true // 中心座標の調整を有効化
});
console.log('中心固定回転の処理が完了しました');
} catch (error) {
console.error('プログラムの実行に失敗しました:', error);
}
}
main();
結果
入力 | 出力(1) | 出力(2) |
---|---|---|
![]() |
![]() |
![]() |
Discussion