OpenCVでヒストグラムを作成する（C++）

# はじめに
画像処理技術では、画像のカラーがどのような分布か見ることがある。その分布を見るためにヒストグラムを作成する必要がある。本稿では、ヒストグラムとはなんであるかの説明と、実際にC++でヒストグラムを作成するプログラムを紹介する。

## 環境
本稿で紹介するプログラムは下記の環境で行なった。

- OS：MacOS Mojave (Version10.14.6)
- CPU：1.3GHz Intel Core m7
- メモリ：8GB / DDR3
- Shell：zsh
- ライブラリ：OpenCV version3.4.0

OpenCVの環境設定やその他の環境設定は他のサイトを参考にしてほしい。特にOSによって設定方法が異なるため、自分の環境に正しい設定を行う必要がある。googleで「OpenCV3 Mac 環境」などと検索すると多くのサイトが見つかる。

## 準備
C++でプログラムを書く場合は、コンパイル作業が必要になる。コンパイルが正しく行えるかを確認するには下記のコマンドを利用する。

```zsh

mask0$ pkg-config --cflags opencv
-I/usr/local/include/opencv -I/usr/local/include

mask0$ pkg-config --libs opencv 
-L/usr/local/lib -lopencv_dnn -lopencv_ml -lopencv_objdetect -lopencv_shape -lopencv_stitching -lopencv_superres -lopencv_videostab -lopencv_calib3d -lopencv_features2d -lopencv_highgui -lopencv_videoio -lopencv_imgcodecs -lopencv_video -lopencv_photo -lopencv_imgproc -lopencv_flann -lopencv_core


```

正しく環境が整っていれば、出力結果が得られる。誤っていれば、エラーが出力される。なお、出力結果の順番や多少の違いはあるかもしれないが、エラーが出ていなければ問題ない。

## コンパイル
C++の場合、コンパイル作業が必要になる。コンパイルは下記のコマンドで行う。

```zsh
g++ main.cpp `pkg-config --cflags opencv` `plg-config --libs opencv`
```

# ヒストグラムとは
ヒストグラムとは画像処理分野以外でも利用することもある。単直に言ってしまえば“グラフ”である。画像処理分野では各濃度値に対して、その濃度値を持ったピクセルが何個あるのかをグラフ化したものである。

横軸に濃度値（0〜255）、縦軸にピクセル数（以下、画素数と呼ぶ）をとる。

画像にはピクセル1つ1つに色情報がある。カラー画像の場合、RGBの3チャンネルの値がそれぞれ入っている。

## 例
文章だけではわかりづらいためここでは簡単な例を挙げて説明する。下記のような４×4ピクセルの画像があるとし、それぞれのピクセルの値が下記の通りだとする。

<table style="border-collapse: collapse; width: 300pt;" border="0" width="400" cellspacing="0" cellpadding="0">
<colgroup>
<col style="width: 75pt;" span="4" width="100"> </colgroup>
<tbody>
<tr style="height: 63.0pt;">
<td class="xl63" style="height: 63.0pt; width: 75pt;" width="100" height="84">R：0<br>G：1<br>B：2</td>
<td class="xl63" style="width: 75pt;" width="100">R：1<br>G：1<br>B：3</td>
<td class="xl63" style="width: 75pt;" width="100">R：7<br>G：3<br>B：6</td>
<td class="xl63" style="width: 75pt;" width="100">R：1<br>G：2<br>B：5</td>
</tr>
<tr style="height: 63.0pt;">
<td class="xl63" style="height: 63.0pt; width: 75pt;" width="100" height="84">R：9<br>G：5<br>B：6</td>
<td class="xl63" style="width: 75pt;" width="100">R：8<br>G：3<br>B：4</td>
<td class="xl63" style="width: 75pt;" width="100">R：7<br>G：2<br>B：3</td>
<td class="xl63" style="width: 75pt;" width="100">R：2<br>G：0<br>B：6</td>
</tr>
<tr style="height: 63.0pt;">
<td class="xl63" style="height: 63.0pt; width: 75pt;" width="100" height="84">R：1<br>G：4<br>B：5</td>
<td class="xl63" style="width: 75pt;" width="100">R：8<br>G：7<br>B：9</td>
<td class="xl63" style="width: 75pt;" width="100">R：3<br>G：2<br>B：3</td>
<td class="xl63" style="width: 75pt;" width="100">R：1<br>G：4<br>B：3</td>
</tr>
<tr style="height: 63.0pt;">
<td class="xl63" style="height: 63.0pt; width: 75pt;" width="100" height="84">R：0<br>G：3<br>B：5</td>
<td class="xl63" style="width: 75pt;" width="100">R：1<br>G：4<br>B：5</td>
<td class="xl63" style="width: 75pt;" width="100">R：1<br>G：2<br>B：5</td>
<td class="xl63" style="width: 75pt;" width="100">R：3<br>G：4<br>B：6</td>
</tr>
</tbody>
</table>

この画像のそれぞれの色について数を数えると下記のようになる。

<table>
<tbody>
<tr>
<td width="64">色の値</td>
<td width="48">R</td>
<td width="48">G</td>
<td width="48">B</td>
</tr>
<tr>
<td>0</td>
<td>2個</td>
<td>1個</td>
<td>0個</td>
</tr>
<tr>
<td>1</td>
<td>6個</td>
<td>2個</td>
<td>0個</td>
</tr>
<tr>
<td>2</td>
<td>1個</td>
<td>4個</td>
<td>1個</td>
</tr>
<tr>
<td>3</td>
<td>2個</td>
<td>3個</td>
<td>4個</td>
</tr>
<tr>
<td>4</td>
<td>0個</td>
<td>4個</td>
<td>1個</td>
</tr>
<tr>
<td>5</td>
<td>0個</td>
<td>1個</td>
<td>5個</td>
</tr>
<tr>
<td>6</td>
<td>0個</td>
<td>0個</td>
<td>4個</td>
</tr>
<tr>
<td>7</td>
<td>2個</td>
<td>1個</td>
<td>0個</td>
</tr>
<tr>
<td>8</td>
<td>2個</td>
<td>0個</td>
<td>0個</td>
</tr>
<tr>
<td>9</td>
<td>1個</td>
<td>0個</td>
<td>1個</td>
</tr>
</tbody>
</table>

そして、この個数をグラフ化するとヒストグラムになる。ヒストグラムは下記のようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472629/193230d6-c5a2-598a-f906-9f3bbe4c07e3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472629/3bef4624-037f-ffb9-1ee4-171d3f09f06b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472629/26b88fd4-b7d0-34e0-90a9-baf8b3c89944.png)

もちろん、本物の画像には色の値は0〜255あるので横軸は“0〜255”である。さらに個数も多くなるため縦軸はもっと長い。画像のサイズが大きければ大きいほど、画素数が増えるため、1000単位の個数になることもある。

## ヒストグラムは何に使う？
ヒストグラムが理解できたところで、疑問に思う人もいるだろう。「これって何に使えるの？」と。私の記事は１から画像処理について理論を中心に説明をしてきたが、画像処理を行って一体何をするのかについて触れていない。ここでは、一昔前の画像処理の利用について触れる。

<font color="FireBrick">人間は目で見たものを認識することができる。</font>車が走っている、コップがある、これは飛行機だ、英語で書かれている、などなど。
しかし、<font color="MediumBlue">__コンピュータは__画像をインプットされたところで、__そこに何が写っているのか認識することができない。__</font>機械学習が発展し、物体の検出などが可能になったのはつい最近のことなのだ。

それまではどうやって物体を検出しようとしていたか。それは<font color="DarkRed">__色情報__</font>である。例えば、画像から人間の手を検出しようと思えば、肌色の情報だけを抜きだせば、誤検出はあるかもしれないが、ある程度環境が整った（検出しやすい環境）画像から検出できるだろう。他にも風景の写真から葉っぱだけを検出したい場合は、”緑色のこの値に葉っぱが多く含まれている”と検出することができる。

そのためにヒストグラムを用いて、色情報分布を見ることで調査し、閾値を決定するのである。一昔前の画像処理方法と説明したが、現在でも色情報を用いた処理は多く行われている。

# ヒストグラム作成
ここからは実際にヒストグラムを作成するプログラムを紹介する。

## スプリット処理

ヒストグラムを作成する前に、画像をチャンネル別（色別）に分ける必要がある。先ほどの説明の通り、ヒストグラムはカラー画像の場合、それぞれの色情報にまず分けなければ作成することはできない。OpenCVには画像を1つのチャンネル毎に分ける処理を行う関数が用意されている。

### プログラム（C++）
```c++

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;


int main(int argc,char *argv[]) {

  Mat src;         // 画像の読み込み変数
  Mat channels[3]; // チャンネル毎に分けた後に代入する変数

  src = imread(argv[1]);  // 画像の読み込み
  imshow("source", src);  // 画像表示

  split(src, channels);   // 入力画像を3チャンネルに分解</span>

  imshow("B", channels[0]);  // 画像表示
  imshow("G", channels[1]);  // 画像表示
  imshow("R", channels[2]);  // 画像表示

  waitKey(0);

  return 0;

}
```
split()関数は第1引数の３チャネル画像を、第2引数のMat型配列に１チャネル毎格納することができる関数である。結果は下記の通りである。（背景は気にしないで欲しい。。。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472629/3a130b76-7dc6-884d-0fd1-2165d15113dd.png)
上の画像が元の画像である。下の左から順番にR、G、Bの値のみの情報で構成された画像である。

## ヒストグラム作成プログラム
### 構成
構成は主に下記の2つの関数である。

- CreateHistgram()関数
- main()関数

ヒストグラムの作成は全部_CreateHistgram()関数_で行う。_main()関数_は画像の読み込みと表示のみを行う。

### プログラム
ヒストグラムを作成するプログラムは自分の好み次第で綺麗にできる。私の場合、綺麗に見せるために無駄にラインを描いたりしている。もっと綺麗にしたい人は好みで線を引いたり、カラーを変えたりすると良い。

```c++

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;


/*************************************************
 * Mat CreateHistgram(Mat channels[3])
 * Mat channels[3] : split後のチャンネル
 *
 * 機能 : ヒストグラムを作成
 *
 * return :　image_hist (ヒストグラムのグラフ画像)
 *************************************************/
Mat CreateHistgram(Mat channels[3]){

  /* 変数宣言 */
  Mat image_hist;
  Mat R, G, B;
  int hist_size = 256;
  float range[] = {0, 256};
  const float* hist_range = range;

  /* 画素数を数える */
  calcHist(&channels[0], 1, 0, Mat(), R, 1, &hist_size, &hist_range);
  calcHist(&channels[1], 1, 0, Mat(), G, 1, &hist_size, &hist_range);
  calcHist(&channels[2], 1, 0, Mat(), B, 1, &hist_size, &hist_range);

  /* 正規化 */
  normalize(R, R, 0.0, 1.0, NORM_MINMAX, -1, Mat());
  normalize(G, G, 0.0, 1.0, NORM_MINMAX, -1, Mat());
  normalize(B, B, 0.0, 1.0, NORM_MINMAX, -1, Mat());

  /* ヒストグラム生成用の画像を作成 */
  image_hist = Mat(Size(276,320), CV_8UC3, Scalar(255,255,255));

   /* 背景を描画（見やすくするためにヒストグラム部分の背景をグレーにする） */
  for(int i = 0; i < 3; i++){
    rectangle(image_hist, Point(10, 10 + 100 * i),
              Point(265, 100 + 100 * i), Scalar(230, 230, 230), -1);
  }

  /* ヒストグラムを描画 */
  for(int i = 0; i < 256; i++){
    // それぞれのヒストグラムを描画
    line(image_hist, Point(10+i, 100),
         Point(10+i, 100-(int)(R.at<float>(i) * 80)),
         Scalar(0,0,255), 1, 8, 0);

    line(image_hist, Point(10+i, 200),
         Point(10+i, 200-(int)(G.at<float>(i) * 80)),
         Scalar(0,255,0), 1, 8, 0);

    line(image_hist, Point(10+i, 300),
         Point(10+i, 300-(int)(G.at<float>(i) * 80)),
         Scalar(255,0,0), 1, 8, 0);


    // 横軸10ずつラインを引く
    if(i%10 == 0){
      line(image_hist, Point(10+i, 100), Point(10+i, 10),
           Scalar(170,170,170), 1, 8, 0);
      line(image_hist, Point(10+i, 200), Point(10+i, 110),
           Scalar(170,170,170), 1, 8, 0);
      line(image_hist, Point(10+i, 300), Point(10+i, 210),
           Scalar(170,170,170), 1, 8, 0);

      // 横軸50ずつ濃いラインを引く
      if(i%50 == 0){
        line(image_hist, Point(10+i, 100), Point(10+i, 10),
             Scalar(50,50,50), 1, 8, 0);
        line(image_hist, Point(10+i, 200), Point(10+i, 110),
             Scalar(50,50,50), 1, 8, 0);
        line(image_hist, Point(10+i, 300), Point(10+i, 210),
             Scalar(50,50,50), 1, 8, 0);
      }
    }
  }
  return image_hist;
}

/* main関数 */
int main(int argc,char *argv[]) {

  Mat src; // 画像の読み込み変数
  Mat channels[3]; // チャンネル毎に分けた後に代入する変数

  src = imread(argv[1]); // 画像の読み込み
  imshow("source", src);  // 元画像の画像表示

  split(src, channels); // 入力画像を3チャンネルに分解
  Mat histgram = CreateHistgram(channels); // ヒストグラム作成
  imshow("histgram", histgram);  // ヒストグラム表示

  waitKey(0);
  return 0;
}
```

### 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472629/20afeceb-f49e-5be1-db9c-1da70eb4fd90.png)
上から順番にR,G,Bの画素数をグラフ化したヒストグラムである。
もっと綺麗に見せる方法として、縦軸に数字を書いたり、それぞれのグラフに”R”、”G”、”B”とルビを振るなどが考えられる。最低限のレベルには達していると思うので、良しとする。

# 終わりに
今回は画像処理分野において“ヒストグラムとはなんなのか”についてと、実際にヒストグラムを作成するプログラムを紹介した。

画像処理で色情報を使うことはよくあり、そのためにヒストグラムによる調査を行うこともよくある。現在はなんでも機械学習に突っ込んでしまえば、物体検出や文字の解読などが容易にできるが、その機械学習に入力する（学習させる）画像に無駄が多いと、検出率も悪くなる。無駄をなくすために色情報を使って、ノイズの除去をするためにも用いられる。

## 私事ですが
[人生何とでもなれ-新卒4ヶ月で退職、再就職までの道](http://mask0-create.com/ "人生何とでもなれ-新卒4ヶ月で退職、再就職までの道")

私事ではございますが、こちらに書いた記事は詳しく、自身のブログにまとめております。
また、機械学習についてや、gitなど様々な技術分野をわかりやすく解説しております。
よければ、アクセスしてみください。