📷

画像処理100本ノックに挑戦|アフィン変換(平行移動)(028/100)

2025/01/30に公開

これはなに?

画像処理100本ノックを、TypeScriptとlibvipsで挑戦してみる記事の28本目です。

前回

https://zenn.dev/nyagato_00/articles/5d252ed19206f8

実装

お題

アフィン変換を利用して画像をx方向に+30、y方向に-30だけ平行移動させよ。

アフィン変換とは3x3の行列を用いて画像の変換を行う操作である。

https://github.com/minido/Gasyori100knock-1/tree/master/Question_21_30#q28-アフィン変換平行移動

Coding

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
}

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 * params.a);
    const newHeight = Math.round(height * params.d);

    // 新しいバッファーを作成(黒で初期化)
    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('アフィン変換行列が特異です');
    }

    // 各ピクセルに対して変換を適用
    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
        );

        // 元画像の範囲内かチェック
        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;
  }
}

結果

入力 出力

Discussion