画像のヒストグラムの可視化をしてみる

2021年1月24日

こんにちは。今回は、2値化の前にヒストグラムの実装をして、画像画素がRGBやグレースケールの段階のどのくらいの値にあるのかどうかの確認をしてみたいと思います。ヒストグラムが2値化をするときに閾値を決めるときに必要になってくるのではないかと考えたためです。

私事にはなりますが、自由時間を画像処理と英語の勉強等をして、辛くなったらマイクロマウスの調整や制御プログラムをいじったりをしてリフレッシュするということをしています1。移動時間は、とある方にお勧めしていただいた"本好きの下剋上 ~司書になるためには手段を選んでいられません~"という小説家になろうで掲載されている小説をひたすらに読んでいます。読むのに集中して、電車を乗り過ごすことを複数回あるのでどうにかしないとも思いつつも学習をせずにやらかしています。何かに夢中になって取り組んだことのある方におすすめです。

閑話休題。さて、ヒストグラムの基礎から追っていきたいと思います。




基礎編

ヒストグラムとは

横軸に画素値を、縦軸にそれぞれの画素値の頻度(その画素値をもつ画素の個数)をとり、各画像の画素値の分布を棒グラフで表したものである。

参考:ディジタル画像処理[改訂新版] P58より

数学で言うところの度数グラムを表すグラフのひとつであるヒストグラムの画像の画素値バージョンのようです。度数グラフという時点で、だいたいの実装方法が理解できたと思います。実装を行う前に、ヒストグラムを壁画する上で簡単なテクニック2を書いておきたいと思います。

ヒストグラムの実装の方針について

画素の値は0~255の256段階で表されており、グレースケールでは0のときに黒、255のときに白になります。BGR3で画像が表現されているときは、青、緑、赤も同様に、0のときに画素が存在しない、255のときに青、緑、赤がくっきりとでてくるという形になっています。

頻度を求める方法は、256個の配列を作成し、0で初期化。画像の縦横をすべて探索して配列の画素の値の配列をインクリメントしていくという方法です。実装に関しては、この後にでてくるソースコードを見ていただければと思います。

ヒストグラムの壁画を行うにあたって

OpenCVにヒストグラムの壁画を行う関数は実装されていないそうなので、自力で実装する必要が出てきます。OpenCVのサイトではレクタングルで実装されていましたが、OpenCVの線を引く関数のほうが簡単に実装できたのでline関数を使用しています。

ここで問題がでてきました。度数分布のままでは値が画像によって変化するため、線を引くときに上手く引けないという問題が生じます。そこで、最大値でわって0~1の範囲内に抑え込む処理4をしておきます。

実装編

開発環境等についてはこちらを見ていただければと思います。

コード確認

実装したソースコードを以下に示します。

今回は、ヒストグラムの計算とヒストグラムの棒線グラフを作成したグラフの画像データを返すプログラムを別に作成しました。理由として、ヒストグラムを変化させる処理を行うときにこの関数を有効活用しやすいようにするためです。colorの引数の初期値を1に設定してあるため基本的にはカラー画像の処理を自動で行うようにしています。

#pragma once

#include <opencv2/opencv.hpp>

#include <iostream>

std::vector<std::vector<float>> myCalcHistogram(cv::Mat data, int8_t color = 1);
cv::Mat myPaintHistgram(std::vector<std::vector<float>> data, int8_t color = 1);
#include "myHistgram.h"

using namespace cv;
using namespace std;

vector<vector<float>> myCalcHistogram(Mat data, int8_t color)
{
	int width = data.size().width;
	int height = data.size().height;

	if (color == 1) {
		int red, green, blue;
		int red_hist[256], green_hist[256], blue_hist[256];

		for (int i = 0; i < 256; i++) {
			red_hist[i] = 0;
			green_hist[i] = 0;
			blue_hist[i] = 0;
		}

		for (int x = 0; x < width; x++) {
			for (int y = 0; y < height; y++) {
				blue = data.at<Vec3b>(y, x)[0];
				green = data.at<Vec3b>(y, x)[1];
				red = data.at<Vec3b>(y, x)[2];

				blue_hist[blue]++;
				green_hist[green]++;
				red_hist[red]++;
			}
		}

		float red_hist_max = 0;
		float green_hist_max = 0;
		float blue_hist_max = 0;

		for (int i = 0; i < 256; i++) {
			if (blue_hist[i] > blue_hist_max) blue_hist_max = blue_hist[i];
			if (green_hist[i] > green_hist_max) green_hist_max = green_hist[i];
			if (red_hist[i] > red_hist_max) red_hist_max = red_hist[i];
			
		}
		vector<vector<float>> hist(3, vector<float>(257));
		for (int i = 0; i < 256; i++) {
			hist[0][i] = (float)(blue_hist[i] / blue_hist_max);
			hist[1][i] = (float)(green_hist[i] / green_hist_max);
			hist[2][i] = (float)(red_hist[i] / red_hist_max);
		}

		// 正規化に使用した値を保存しておく
		hist[0][256] = blue_hist_max;
		hist[1][256] = green_hist_max;
		hist[2][256] = red_hist_max;

		return hist;
	}
	else {
		int gray;
		int gray_hist[256];
		for (int i = 0; i < 256; i++) gray_hist[i] = 0;

		for (int x = 0; x < width; x++) {
			for (int y = 0; y < height; y++) {
				gray = data.at<uchar>(y, x);
				gray_hist[gray]++;
			}
		}

		float gray_hist_max = 0.0f;

		for (int i = 0; i < 256; i++) {
			if (gray_hist[i] > gray_hist_max) gray_hist_max = gray_hist[i];
		}

		vector<vector<float>> hist(1, vector<float>(257));
		for (int i = 0; i < 256; i++) hist[0][i] = gray_hist[i] / gray_hist_max;

		hist[0][256] = gray_hist_max;
		return hist;
	}	
}

Mat myPaintHistgram(vector<vector<float>> data, int8_t color)
{
	int width = 532;
	int height = 470;
	if (color == 0) {
		height = 180;
	}
	
	Mat histgram = Mat(Size(width, height), CV_8UC3, Scalar(255, 255, 255));

	if (color == 1) {
		for (int i = 0; i < 3; i++) {
			rectangle(histgram, Point(10, 20 + 150 * i), Point(523, 150 + 150 * i), Scalar(220, 220, 220));
		}

		for (int i = 0; i < 256; i++) {
			line(histgram, Point(10 + i * 2, 150), Point(10 + i * 2, 150 - (float)(data[2][i] * 120)), Scalar(0, 0, 255),2);
			line(histgram, Point(10 + i * 2, 300), Point(10 + i * 2, 300 - (float)(data[1][i] * 120)), Scalar(0, 255, 0),2);
			line(histgram, Point(10 + i * 2, 450), Point(10 + i * 2, 450 - (float)(data[0][i] * 120)), Scalar(255, 0, 0),2);
		}
	}
	else {
		rectangle(histgram, Point(10, 20 + 150), Point(523, 150 + 150), Scalar(220, 220, 220));
		for (int i = 0; i < 256; i++) 
			line(histgram, Point(10 + i * 2, 150), Point(10 + i * 2, 150 - (float)(data[0][i] * 120)), Scalar(0, 0, 0),2);
	}

	return histgram;
}

グレースケールのときとカラー画像のときとで同じ処理を2回書いているため、少し冗長になってしまいました。

実行結果

実行結果を以下に示します。

実行に使用したソースコードはこちらです。

#include <opencv2/opencv.hpp>
#include <iostream>

#include "myHistgram.h"
int main()
{
	Mat data;
	data = imread("Image/2.png");
	if (data.empty()) {
		cout << "Image Data is empty!" << endl;
		return 0;
	}

    imshow("original img", gray);
	imshow("color histgram" ,myPaintHistgram(myCalcHistogram(data)));

	Mat gray;

	cvtColor(data, gray, COLOR_BGR2GRAY);
	imshow("gray", gray);
	imshow("gray histgram", myPaintHistgram(myCalcHistogram(data,0), 0));

	waitKey();
	return 0;
}

さいごに

ヒストグラムを画像から取り出すことができるようになったので、次回は画素ごとの濃淡変換をやっていきたいと思います。

参考文献

ディジタル画像処理[改訂新版]

  1. リフレッシュできているのだろうか?
  2. テクニックといえるほどのことでもないと思いますが・・・
  3. RGBと書くことが多いですが、OpenCVではBGRの順でMatに保存されているためBGRで書いています。
  4. 正規化と呼ばれる処理