📷

画像処理100本ノックに挑戦|Maxプーリング(008/100)

2025/01/09に公開

これはなに?

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

前回

https://zenn.dev/nyagato_00/articles/91ff5e44b4c6dd

実装

お題

ここでは平均値でなく最大値でプーリングせよ。

https://github.com/minido/Gasyori100knock-1/tree/master/Question_01_10#q8-maxプーリング

Coding

import sharp from 'sharp';

export async function maxPooling(
  inputPath: string, 
  outputPath: string, 
  gridSize: number = 8
): Promise<void> {
  try {
    const image = await sharp(inputPath)
      .raw()
      .toBuffer({ resolveWithObject: true });

    const { data, info } = image;
    const { width, height, channels } = info;

    // Grid数を計算
    const Nh = Math.floor(height / gridSize);
    const Nw = Math.floor(width / gridSize);

    // 元画像をコピー
    const newData = Buffer.from(data);

    // 各グリッドに対して処理
    for (let y = 0; y < Nh; y++) {
      for (let x = 0; x < Nw; x++) {
        for (let c = 0; c < channels; c++) {
          // グリッド内の最大値を求める
          let maxValue = 0;
          for (let dy = 0; dy < gridSize; dy++) {
            for (let dx = 0; dx < gridSize; dx++) {
              const pos = ((y * gridSize + dy) * width + (x * gridSize + dx)) * channels + c;
              maxValue = Math.max(maxValue, data[pos]);
            }
          }

          // グリッド内の全ピクセルに最大値を設定
          for (let dy = 0; dy < gridSize; dy++) {
            for (let dx = 0; dx < gridSize; dx++) {
              const pos = ((y * gridSize + dy) * width + (x * gridSize + dx)) * channels + c;
              newData[pos] = maxValue;
            }
          }
        }
      }
    }

    // 結果を保存
    await sharp(newData, {
      raw: {
        width,
        height,
        channels
      }
    })
    .toFile(outputPath);

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

Test

import { existsSync, unlinkSync } from 'fs';
import { join } from 'path';
import sharp from 'sharp';
import { maxPooling } from './imageProcessor';

describe('Max Pooling Tests', () => {
  const testInputPath = join(__dirname, '../test-images/test.jpeg');
  const testOutputPath = join(__dirname, '../test-images/test-maxpooled.png');

  afterEach(() => {
    if (existsSync(testOutputPath)) {
      unlinkSync(testOutputPath);
    }
  });

  test('should successfully process image', async () => {
    await expect(maxPooling(testInputPath, testOutputPath))
      .resolves.not.toThrow();
    expect(existsSync(testOutputPath)).toBe(true);
  });

  test('should maintain image dimensions', async () => {
    await maxPooling(testInputPath, testOutputPath);
    
    const inputMetadata = await sharp(testInputPath).metadata();
    const outputMetadata = await sharp(testOutputPath).metadata();

    expect(outputMetadata.width).toBe(inputMetadata.width);
    expect(outputMetadata.height).toBe(inputMetadata.height);
  });

  test('should use maximum value in each grid', async () => {
    const gridSize = 8;
    await maxPooling(testInputPath, testOutputPath, gridSize);
    
    const outputImage = await sharp(testOutputPath)
      .raw()
      .toBuffer({ resolveWithObject: true });

    const { data, info } = outputImage;
    const { width, channels } = info;

    // テスト用のグリッドを選択
    const startX = gridSize;
    const startY = gridSize;

    // グリッド内の最初のピクセル値を取得
    const firstPixelPos = (startY * width + startX) * channels;
    const referenceValues = [
      data[firstPixelPos],
      data[firstPixelPos + 1],
      data[firstPixelPos + 2]
    ];

    // グリッド内の全ピクセルが同じ値(最大値)を持つことを確認
    for (let dy = 0; dy < gridSize; dy++) {
      for (let dx = 0; dx < gridSize; dx++) {
        const pos = ((startY + dy) * width + (startX + dx)) * channels;
        for (let c = 0; c < channels; c++) {
          expect(data[pos + c]).toBe(referenceValues[c]);
        }
      }
    }
  });

  test('should use actual maximum values', async () => {
    const gridSize = 8;
    
    // 入力画像の値を取得
    const inputImage = await sharp(testInputPath)
      .raw()
      .toBuffer({ resolveWithObject: true });

    await maxPooling(testInputPath, testOutputPath, gridSize);
    
    const outputImage = await sharp(testOutputPath)
      .raw()
      .toBuffer({ resolveWithObject: true });

    // テスト用のグリッドを選択してチェック
    const startX = gridSize;
    const startY = gridSize;
    const { width, channels } = inputImage.info;

    // 入力画像のグリッド内の最大値を計算
    const maxValues = new Array(channels).fill(0);
    for (let dy = 0; dy < gridSize; dy++) {
      for (let dx = 0; dx < gridSize; dx++) {
        const pos = ((startY + dy) * width + (startX + dx)) * channels;
        for (let c = 0; c < channels; c++) {
          maxValues[c] = Math.max(maxValues[c], inputImage.data[pos + c]);
        }
      }
    }

    // 出力画像の値が最大値と一致することを確認
    const outputPos = (startY * width + startX) * channels;
    for (let c = 0; c < channels; c++) {
      expect(outputImage.data[outputPos + c]).toBe(maxValues[c]);
    }
  });
});

結果

入力 結果

Discussion