📷

画像処理100本ノックに挑戦|Bi-cubic補間(027/100)

2025/01/29に公開

これはなに?

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

前回

https://zenn.dev/nyagato_00/articles/b3355f806fb6ad

実装

お題

Bi-cubic補間により画像を1.5倍に拡大せよ。

Bi-cubic補間とはBi-linear補間の拡張であり、周辺の16画素から補間を行う。

https://github.com/minido/Gasyori100knock-1/tree/master/Question_21_30#q27-bi-cubic補間

Coding

import sharp from 'sharp';

function calculateWeight(t: number): number {
  const a = -1.0;
  const absT = Math.abs(t);
  
  if (absT <= 1) {
    return (a + 2) * Math.pow(absT, 3) - (a + 3) * Math.pow(absT, 2) + 1;
  } else if (absT <= 2) {
    return a * Math.pow(absT, 3) - 5 * a * Math.pow(absT, 2) + 8 * a * absT - 4 * a;
  }
  return 0;
}

export async function bicubicInterpolation(
  inputPath: string,
  outputPath: string,
  scaleX: number = 1.5,
  scaleY: number = 1.5
): 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 * scaleX);
    const newHeight = Math.round(height * scaleY);
    const newData = Buffer.alloc(newWidth * newHeight * channels);

    // 各ピクセルについて補間を計算
    for (let y = 0; y < newHeight; y++) {
      for (let x = 0; x < newWidth; x++) {
        // 元の座標を計算(浮動小数点)
        const srcX = x / scaleX;
        const srcY = y / scaleY;

        // 基準となるピクセルの座標
        const ix = Math.floor(srcX);
        const iy = Math.floor(srcY);

        // 距離の計算
        const dx = srcX - ix;
        const dy = srcY - iy;

        // 各チャネルについて16点の補間を実行
        for (let c = 0; c < channels; c++) {
          let sum = 0;
          let weightSum = 0;

          // 16点のピクセルを使用
          for (let jy = -1; jy <= 2; jy++) {
            for (let jx = -1; jx <= 2; jx++) {
              // 参照ピクセルの座標
              const px = Math.min(Math.max(ix + jx, 0), width - 1);
              const py = Math.min(Math.max(iy + jy, 0), height - 1);

              // 重みの計算
              const wx = calculateWeight(jx - dx);
              const wy = calculateWeight(jy - dy);
              const weight = wx * wy;

              // ピクセル値の取得と重み付け
              const pos = (py * width + px) * channels + c;
              sum += weight * data[pos];
              weightSum += weight;
            }
          }

          // 結果の正規化と保存
          const newPos = (y * newWidth + x) * channels + c;
          newData[newPos] = Math.min(255, Math.max(0, Math.round(sum / weightSum)));
        }
      }
    }

    await sharp(newData, {
      raw: {
        width: newWidth,
        height: newHeight,
        channels
      }
    })
    .toFile(outputPath);

    console.log('バイキュービック補間による拡大が完了しました');
    console.log(`元のサイズ: ${width}x${height}`);
    console.log(`拡大後のサイズ: ${newWidth}x${newHeight}`);

  } catch (error) {
    console.error('画像処理中にエラーが発生しました:', error);
    throw error;
  }
}

結果

入力 出力

Discussion