📷

画像処理100本ノックに挑戦|ヒストグラム表示(020/100)

2025/01/21に公開

これはなに?

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

前回

https://zenn.dev/nyagato_00/articles/34c942089987d7

実装

お題

matplotlibを用いてimori_dark.jpgのヒストグラムを表示せよ。

ヒストグラムとは画素の出現回数をグラフにしたものである。 matplotlibではhist()という関数がすでにあるので、それを利用する。

https://github.com/minido/Gasyori100knock-1/tree/master/Question_11_20#q20-ヒストグラム表示

Coding

import sharp from 'sharp';

export async function generateHistogramData(inputPath: string): Promise<number[]> {
  try {
    // 画像を読み込む
    const image = await sharp(inputPath)
      .raw()
      .toBuffer({ resolveWithObject: true });

    const { data } = image;

    // ヒストグラムデータの初期化(0-255の範囲)
    const histogram = new Array(256).fill(0);

    // 各ピクセル値の出現回数をカウント
    for (const pixel of data) {
      histogram[pixel]++;
    }

    return histogram;
  } catch (error) {
    console.error('ヒストグラム生成中にエラーが発生しました:', error);
    throw error;
  }
}

matplotlibではなくて、コンソールでサクッと確認できるように、ASCIIアートでヒストグラムを視覚化するようにしてみました。

import * as path from 'path';
import { generateHistogramData } from './imageProcessor';

 function displayHistogramAscii(histogram: number[]) {
  const maxCount = Math.max(...histogram);
  const height = 20; // グラフの高さ
  const width = 80;  // グラフの幅
  const binSize = Math.ceil(256 / width); // ビンの大きさ

  // データを圧縮(ビニング)
  const compressedData: number[] = [];
  for (let i = 0; i < width; i++) {
    const start = i * binSize;
    const end = Math.min(start + binSize, 256);
    const sum = histogram.slice(start, end).reduce((a, b) => a + b, 0);
    compressedData.push(sum);
  }

  const maxCompressed = Math.max(...compressedData);

  // ヒストグラムを描画
  console.log('\nヒストグラム表示 (横軸: ピクセル値 0-255, 縦軸: 出現頻度)');
  console.log('=' . repeat(width + 2));

  for (let h = height - 1; h >= 0; h--) {
    process.stdout.write('|');
    for (let w = 0; w < width; w++) {
      const normalizedValue = compressedData[w] / maxCompressed;
      const threshold = h / height;
      process.stdout.write(normalizedValue > threshold ? '█' : ' ');
    }
    process.stdout.write('|\n');
  }

  console.log('=' . repeat(width + 2));
  console.log(`0${' '.repeat(width-2)}255`);

  // 統計情報を表示
  const total = histogram.reduce((a, b) => a + b, 0);
  const mean = histogram.reduce((acc, count, value) => acc + value * count, 0) / total;
  console.log(`\n統計情報:`);
  console.log(`総ピクセル数: ${total}`);
  console.log(`平均値: ${mean.toFixed(2)}`);
  console.log(`最大出現回数: ${maxCount} (値: ${histogram.indexOf(maxCount)})`);
}

async function main() {
  // __dirnameを使用して相対パスを解決
  const inputPath = path.join(__dirname, '../main.jpeg');
  
  try {
    console.log('画像処理を開始します...');

    const histogram = await generateHistogramData(inputPath);
    
    // ASCII アートでヒストグラムを表示
    displayHistogramAscii(histogram);
    
    // 詳細な数値も表示
    console.log('\n詳細な出現回数:');
    histogram.forEach((count, value) => {
      if (count > 0) {
        console.log(`${value.toString().padStart(3, ' ')}: ${count.toString().padStart(6, ' ')}`);
      }
    });
    console.log('処理が完了しました');
  } catch (error) {
    console.error('プログラムの実行に失敗しました:', error);
  }
}

main();

結果

入力 出力

Discussion