【C言語】画像をグレースケール化

画像のグレースケール化の解説ページアイキャッチ

このページでは、画像をグレースケールに変換するための原理とC言語プログラムの実例の紹介を行います。

紹介するプログラムを実行することで、下の図のようにカラー画像をグレースケール画像に変換することができます。

グレースケール化のイメージ図

雰囲気あっていいね!

興味出てきたよ!

そうだね!

グレースケール化は画像を扱うプログラムの中でも簡単な部類なので、画像処理プログラミングの入門としてもうってつけだよ!

グレースケール化を行うだけであれば、割と画像を扱うプログラミングとしては簡単な部類になると思います(より綺麗にグレースケール化しようと思うと奥が深いですが…)。

ということで、このグレースケール化について解説していきたいと思います。

MEMO

グレースケール化を行う画像のサンプルとしてリズム727さんによる写真ACからの写真を使用させていただいています

グレースケール画像とは

まずは、グレースケール画像という言葉に聞き慣れない方もおられると思いますので「グレースケール画像」について解説したいと思います。

確かに聞き慣れない言葉だね…

要は「白」〜「灰色」〜「黒」の画素から構成される画像のことだよ!

詳細を説明していくね!

カラー画像

まずはグレースケール画像と対比するためにもカラー画像について解説しておきます。

下記ページで、カラー画像の画素は RGB の3要素の輝度から成り立っていると説明しました。

画像データの解説ページアイキャッチ画像データの構造・画素・ビットマップデータについて解説

この各画素の R と G と B それぞれの輝度値に注目すると、カラー画像ではこの3つの輝度値として「異なる値」が設定されます(同じ値を設定することもできます)。

そして、この RGB の輝度値の組み合わせでさまざまな色を表現することができます。

多くの画像では、RGB のそれぞれの輝度値が 0255256 段階で表されます(このような画像を 24 bit カラー画像などと呼びます)。

例えば RGB の輝度値を (R の輝度値, G の輝度値, B の輝度値)で表したとき、代表的な色は下記のように表現することができます。

  • 赤:(255, 0, 0)
  • 緑:(0, 255, 0)
  • 青:(0, 0, 255)
  • 黄:(255, 255, 0)
  • 紫:(255, 0, 255)
  • 白:(255, 255, 255)
  • 黒:(0, 0, 0)

ここで紹介したのはほんの一部で、1色の輝度値が 256 段階で表現されるとき、256 x 256 x 256 = 16777216 パターンの色を表現することができます。

スポンサーリンク

グレースケール画像

グレースケール画像は各画素がグレーの濃淡のみで表現される画像になります。

より具体的にはグレースケール画像は下記のような画像になります。

  • 画像の各画素の RGB 輝度値が同じ(R=G=B)

もしくは下記のような画像になります。

  • 画像の各画素の輝度値が Gray の1つのみ

カラー画像においても、RGB の輝度値が全て同じ画素では、「白」〜「グレー」〜「黒」のグレーの濃淡のみを表すことになります。

で、画像の全ての画素がグレーの濃淡のみを表す場合(つまり全ての画素の輝度値が R=G=B の関係を満たす場合)、その画像はグレースケール画像であると言えます。

この R=G=B を満たす必要があるため、RGB のそれぞれの輝度値が 0255256 段階で表される場合でも、グレースケール画像においては表現できる色は 256 パターンのみになります。

輝度値が 0 の時は黒、輝度値が 255 の時は白色を表し、その間の色はグレーで、輝度値が増えるほど明るいグレーになっていきます。

グレースケールにおける輝度値の変化

また、グレースケール画像においては、各画素の RGB それぞれの輝度値が同じなので、もはや3つの輝度値で表現する必要もありません。

なので、画像の各画素の輝度値を1つのみ(Gray)として表現することも可能です。

画像の各画素の輝度値が1つのみなので、画像のデータサイズを小さくすることも可能です。

ですが、今回は簡単のため、グレースケール画像は下記のように輝度値3つから画素が構成されるものとして扱っていきたいと思います。

  • 画像の各画素の RGB 輝度値が同じ(R=G=B)

カラー画像からグレースケール画像への変換

ではカラー画像をグレースケール画像に変換する方法について解説していきたいと思います。

前述の通り、グレースケール画像とは下記のような画像になります。

  • 画像の各画素の RGB 輝度値が同じ(R=G=B)

ですので、カラー画像の画素の RGB の輝度値を全て同じ値に設定してやることでグレースケール画像に変換可能です。

つまり、グレースケール化は、カラー画像の全画素を「RGB の輝度値を全て同じ値」にすることで実現することができます。

グレースケール化

では、各画素の RGB の輝度値はいくつにすれば良いでしょうか?

RGB の平均値を求めれば良さそうだけど?

そうだね!それも1つの方法

でも実は他にも方法があるんだ

おそらく「RGB の輝度値を全て同じ値にする」と聞くと、多くの方は RGB の平均値を求めれば良いと考えるのではないかと思います。

私も何も知らずにグレースケール変換プログラムを作った時は平均値を求めて画像を作成するようにしていました。

もちろんこの方法でもグレースケール画像への変換が可能です。

ただし、人間の視覚特性を利用してもっと「人間の感覚に近い」ようにグレースケール変換することも可能です。

このページではグレースケール化するための方法として「RGB の単純平均による方法」と「RGB の重み付けによる方法」それぞれによるプログラムを紹介していきたいと思います。

RGB の単純平均によるグレースケール化

まずは1つ目の「RGB の単純平均によるグレースケール化」について解説します。

スポンサーリンク

スポンサーリンク

考え方

この方法では、単に RGB の輝度値の平均を求めることでグレースケール変換を行います。

グレースケール化後の輝度値
/* r:Rの輝度値,g:Gの輝度値,b:Bの輝度値 */ 
gray = (r + g + b) / 3

より具体的には、グレースケール画像の座標 (x,y) の画素の RGB の輝度値全てを、カラー画像の座標 (x,y) の画素の RGB それぞれの輝度値の平均値に設定することでグレースケール変換を行います。

単純平均によるグレースケール化
r = カラー画像の (x,y) 座標の R
g = カラー画像の (x,y) 座標の G
b = カラー画像の (x,y) 座標の B
gray = (r + g + b) / 3
グレースケール画像の (x,y) 座標の R = gray
グレースケール画像の (x,y) 座標の G = gray
グレースケール画像の (x,y) 座標の B = gray

RGB の単純平均によるグレースケール化プログラム

では RGB の単純平均によってカラー画像をグレースケール画像に変換するプログラムを紹介していきたいと思います。

ソースコード

この方法でカラー画像をグレースケール画像に変換するプログラムのソースコードが下記になります。

main.c
#include "myJpeg.h"

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

  BITMAPDATA_t bitmap;
  int m, n;
  char outname[256];
  unsigned char r, g, b, gray;

  if(argc != 2){
    printf("ファイル名が指定されていません\n");
    return -1;
  }

  if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
    printf("jpegFileReadDecode error\n");
    return -1;
  }

  /* ここからグレースケール変換 */
  for(n = 0; n < bitmap.height; n++){
    for(m = 0; m < bitmap.width; m++){
      /* (m,n)座標のRGBの輝度値を取得 */
      r = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 0];
      g = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 1];
      b = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 2];

      /* 輝度値の平均を計算 */
      gray = (r + g + b) / 3;

      /* (m,n)座標のRGBの輝度値全てにgrayを格納 */
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 0] = gray;
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 1] = gray;
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 2] = gray;
    }
  }
  /* ここまでグレースケール変換 */

  sprintf(outname, "%s", "gray.jpeg");

  if(jpegFileEncodeWrite(&bitmap, outname) == -1){
    printf("jpegFileEncodeWrite error\n");
    freeBitmapData(&bitmap);
    return -1;
  }

  freeBitmapData(&bitmap);

  return 0;
}

コンパイル

コンパイルを行う上で必要なソースコードファイルは下記の3つです。

  • main.c:このページで紹介しているソースコード
  • myJpeg.c:JPEG 読み込み・書き込み用のソースコード
  • myJpeg.h:JPEG 読み込み・書き込みようのヘッダーファイル

myJpeg.cmyJpeg.h は下記ページで公開していますので、コピペして同じファイル名で保存して使用していただければと思います。

libjpegの使い方解説ページアイキャッチ【C言語】libjpegのインストールと使用方法・使用例

また JPEG の読み込みと書き込みを行うため、libjpeg をインストールしておく必要があります。

libjpeg をインストールしておくとC言語でJPEGファイルを使ったプログラムが簡単に作れるようになりますので興味があればインストールしておくことをオススメします。

この libjpeg のインストール方法についても上記のページで紹介していますので、こちらを参考にしてインストールしていただければと思います。

gcc を用いたコマンドラインからのコンパイルは下記で行うことができます。

> gcc myJpeg.c -c
> gcc main.c -c
> gcc myJpeg.o main.o -ljpeg -o main.exe

ljpeg を付けることで libjpeg ライブラリをリンクしています。

実行

プログラムの実行は、コンパイルで生成した実行可能ファイル(main.exe)を下記のように1つの引数を指定して実行します。

./main.exe cat.jpeg

指定する引数は下記になります。

  • 第1引数:入力する JPEG ファイルへのパス

実行すると、グレースケール化後の画像が gray.jpeg という名前の JPEG ファイルとして保存されます。

プログラムの説明

まず下記で jpegFileReadDecode 関数を実行し、引数で渡されたパス JPEG ファイルを読み込みとデコードを行なっています。

JPEG ファイルから BITMAP データ取得
  if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
    printf("jpegFileReadDecode error\n");
    return -1;
  }

デコード後の BITMAP データは BITMAPDATA_t 型の変数 bitmap のメンバである data ポインタが指すことになります。

この data ポインタが指すアドレスのデータをグレースケール化することになります。

また、変数 bitmapdata 以外の各メンバには jpegFileReadDecode 関数内で読み込んだ JPEG 画像(元画像)の情報が格納されます。

具体的には下記の情報が格納されます。

  • width:読み込んだ JPEG 画像の幅
  • height:読み込んだ JPEG 画像の高さ
  • ch:読み込んだ JPEG 画像の色数

この jpegFileReadDecode 関数や BITMAPDATA_t 構造体については下記ページで説明していますので必要に応じて参照してください。

libjpegの使い方解説ページアイキャッチ【C言語】libjpegのインストールと使用方法・使用例

実際のグレースケール変換は下記の部分で行なっています。

グレースケール変換
  /* ここからグレースケール変換 */
  for(n = 0; n < bitmap.height; n++){
    for(m = 0; m < bitmap.width; m++){
      /* (m,n)座標のRGBの輝度値を取得 */
      r = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 0];
      g = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 1];
      b = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 2];

      /* 輝度値の平均を計算 */
      gray = (r + g + b) / 3;

      /* (m,n)座標のRGBの輝度値全てにgrayを格納 */
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 0] = gray;
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 1] = gray;
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 2] = gray;
    }
  }
  /* ここまでグレースケール変換 */

このグレースケール変換を行なっている処理の詳細を説明していきます。

まず、画像の (m, n) 座標の画素の輝度値の先頭にアクセスすることができます。

(m,n) 座標の画素へのアクセス
bitmap.data[bitmap.ch * (m + n * bitmap.width)]

さらに、この配列の添字に下記を足すことにより、それぞれの輝度値にアクセスすることができます。

  • +0:R の輝度値
  • +1:G の輝度値
  • +2:B の輝度値

なので、下記では (m, n) 座標の画素の R と G と B の輝度値の取得していることになります。

輝度値の取得
      /* (m,n)座標のRGBの輝度値を取得 */
      r = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 0];
      g = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 1];
      b = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 2];

で、下記でこの rg と b の平均値を求めることで、グレースケール化後の輝度値 gray を求めています。

グレースケール化後の輝度値の計算
      /* 輝度値の平均を計算 */
      gray = (r + g + b) / 3;

そして、この平均値 gray を下記で、画像の (m, n) 座標の新たな輝度値として設定しています。

平均値の格納
      /* (m,n)座標のRGBの輝度値全てにgrayを格納 */
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 0] = gray;
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 1] = gray;
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 2] = gray;

RGB 全てが同じ値になるので、この画素はグレースケール化後の画素となります。

で、これを nm のループにより画像の全座標に対して行うことで、画像全体がグレースケール化後されることになります。

最後に下記で jpegFileEncodeWrite 関数を実行し、グレースケール化後の画像を JPEG ファイルとして保存しています。

JPEG保存
  if(jpegFileEncodeWrite(&scaledBitmap, outname) == -1){
    printf("jpegFileEncodeWrite error\n");
    freeBitmapData(&bitmap);
    return -1;
  }

この jpegFileEncodeWrite 関数についても下記ページで説明していますので必要に応じて参照してください。

libjpegの使い方解説ページアイキャッチ【C言語】libjpegのインストールと使用方法・使用例

グレースケール化前後の画像

このプログラムにより作成されるグレースケール画像を確認しておきましょう!

グレースケール化前の画像、つまりプログラムの第1引数として指定した画像は ↓ の画像とします。

グレースケール化前の画像

グレースケール化後の画像、つまりプログラム実行後に保存される gray.jpeg は ↓ のようになります。

平均値計算でグレースケール化した結果

RGB の重み付けによるグレースケール変換

続いて2つ目の「RGB の重み付けによるグレースケール化」について解説します。

スポンサーリンク

スポンサーリンク

考え方

この方法では、RGB それぞれの色を同等に扱うのではなく、RGB それぞれに異なった重み付けを行うことでグレースケール変換を行います。

この重み付けは、人間の目の特性に応じて設定します。

例えば下の図を見てみましょう!

色による明るさの違い

黒色背景の上に赤色と緑色と青色の3つの色で文字を書いています。

どの文字が一番明るく見えるでしょうか?

緑かな!

逆に青は暗いね…

青は輝度値が低いのかな?

それぞれの色の輝度値は下記に設定してるよ

  • 赤文字:R=255, G=0, B=0
  • 緑文字:R=0, G=255, B=0
  • 青文字:R=0, G=0, B=255

つまり、それぞれの色に対して輝度値は同じ 255 にしてる

え?

なんでそれでこんなに明るさが違うの?

おそらく先程の画像を見て、緑文字が一番明るく、青文字が一番暗く見えた人が多いのではないかと思います(もちろん個人差はあるかもしれません)。

人間の目は赤、緑、青 それぞれが全て同じ明るさの度合いで見えているわけではなく、緑の方が明るく、青の方が暗く見えるそうです。

ですので、この視覚特性を取り入れた方が、より人の感覚に合ったグレースケール画像が作成できるというわけです。

ここでRGB の単純平均によるグレースケール化で紹介したグレースケール化後の輝度値の求め方を思い出してみましょう!

下記のように単純に平均値を求めるだけでしたね!

グレースケール化後の輝度値
/* r:Rの輝度値,g:Gの輝度値,b:Bの輝度値 */ 
gray = (r + g + b) / 3

この式を変形すると下記のように表すこともできます。

グレースケール化後の輝度値(式変形後)
/* r:Rの輝度値,g:Gの輝度値,b:Bの輝度値 */ 
gray = 0.333... * r  + 0.333... *  g + 0.333... * b

つまり、全ての色に対して同じ重み付けをして(0.333... で掛けて)グレースケール化後の輝度値に求めていることになります。

一方、ここで紹介する方法では、緑である G を一番大きく輝度値に反映し、青である B を一番小さく輝度値に反映するために重み付けを行う値を色ごとに変化させて計算を行います。

そして、その重み付けして計算した結果をグレースケール画像の画素の輝度値に設定します。

これにより、より人間の目の感覚にあったグレースケール化を行うことができます。

で、この重み付けを行う値はいろんな規格で定められています。

今回は下記の式で重み付けすることでグレースケール変換をしていきたいと思います。

グレースケール化後の輝度値
/* r:Rの輝度値,g:Gの輝度値,b:Bの輝度値 */ 
gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;

これは  “ITU-R Rec BT.709” という規格で定められたグレースケール化後の輝度値の計算式とのことです。

他にどのような計算式があるかは下記ページがものすごくわかりやすいです!是非参考にしていろんなグレスケール化に挑戦してください。

グレースケール画像のうんちく

RGB の重み付けによるグレースケール化プログラム

では RGB の重み付けによってカラー画像をグレースケール画像に変換するプログラムを紹介していきたいと思います。

ソースコード

この方法でカラー画像をグレースケール画像に変換するプログラムのソースコードが下記になります。

main.c
#include "myJpeg.h"

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

  BITMAPDATA_t bitmap;
  int m, n;
  char outname[256];
  unsigned char r, g, b, gray;

  if(argc != 2){
    printf("ファイル名が指定されていません\n");
    return -1;
  }

  if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
    printf("jpegFileReadDecode error\n");
    return -1;
  }

  /* ここからグレースケール変換 */
  for(n = 0; n < bitmap.height; n++){
    for(m = 0; m < bitmap.width; m++){
      /* (m,n)座標のRGBの輝度値を取得 */
      r = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 0];
      g = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 1];
      b = bitmap.data[bitmap.ch * (m + n * bitmap.width) + 2];

      /* 重み付けでグレースケール化後の輝度値を計算 */
      gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;

      /* (m,n)座標のRGBの輝度値全てにgrayを格納 */
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 0] = gray;
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 1] = gray;
      bitmap.data[bitmap.ch * (m + n * bitmap.width) + 2] = gray;
    }
  }
  /* ここまでグレースケール変換 */

  sprintf(outname, "%s", "gray.jpeg");

  if(jpegFileEncodeWrite(&bitmap, outname) == -1){
    printf("jpegFileEncodeWrite error\n");
    freeBitmapData(&bitmap);
    return -1;
  }

  freeBitmapData(&bitmap);

  return 0;
}

コンパイル方法や実行方法は先程紹介した RGB の単純平均によるグレースケール化プログラムと同じになりますのでここでの説明は省略します。

プログラムの説明

プログラムもほぼRGB の単純平均によるグレースケール化プログラムと同じです。

違うのは下記のグレースケール化後の画素として設定する gray の値の計算式のみです。

グレースケール化後の輝度値の計算
      /* 重み付けでグレースケール化後の輝度値を計算 */
      gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;

G の輝度値である g に一番大きな係数を、B の輝度値である b に一番小さな係数を掛けて和をとることで、人間の目が敏感に反応する(であろう)緑色の変化を一番大きく反映した輝度値を求めることができます。

前述の通り、グレースケール化を行う際の計算式としては多くのものが提案されています。

グレースケール化を行う際の計算式を変更したいときは、上記の計算式を変更してやれば良いです。いろいろ試してみてください!

グレースケール化前後の画像

このプログラムにより作成されるグレースケール画像を確認しておきましょう!

グレースケール化前の画像、つまりプログラムの第1引数として指定した画像は ↓ の画像とします。

グレースケール化前の画像

グレースケール化後の画像、つまりプログラム実行後に保存される gray.jpeg は ↓ のようになります。

RGBそれぞれに重み付けしてグレースケール化した結果

まとめ

このページでは画像のモノクロ化・グレースケール化の方法とC言語プログラムの例について解説しました。

割と簡単に画像の見た目をガラッと変えられるので画像処理初心者の方にもオススメの題材だと思います。

ぜひ自分で作って画像処理プログラミングの楽しさを実感してみてください!

2 COMMENTS

渋谷義一

画像のグレースケールを調べているウチに貴兄のサイトにたどり着きました。
夜空を連続して撮影する中に、飛行機の軌跡が映り込んでしまいます。
これを1枚1枚、手作業でPhotoshopを使い、軌跡だけを黒く塗りつぶしてから
コンポジットをかけています。
聞くところによると、自動化の方法があるとか。
1.画像のグレースケール化
2.ハフ変換による線分の検出
3.検出した線分を黒く塗りつぶす
だそうですが、貴兄のご教示を賜れば幸甚です。

返信する
daeu

渋谷義一さん

コメントありがとうございます!

おそらく、その方法で自動的に軌跡を塗りつぶすことはできると思います。
が、ちょっと満足いく結果になるかは微妙かなぁという印象です。

Python なんかだと、画像を読み込んで、ハフ変換で線を検出し、さらに線を塗り潰した結果を画像保存してくれるようなプログラムの例がいろんなページで紹介されています。

例えば下記ページなんかは自然画像に対して上記を行った結果が載せられています。

http://rikoubou.hatenablog.com/entry/2019/03/27/202743

渋谷義一さんがやりたいことの場合は、背景画像が星空になり、塗りつぶす色も緑色や赤色でなく黒色になると思うのですが、
何となーく出来上がりのイメージは掴めるのではないのでしょうか?

また、Pythonですがプログラムも公開されているので、実際にご自身の写真で試してみると、やりたいことができそうかどうかがすぐに分かると思います!

パラメータを調整すれば上手くいくかもしれませんが、塗り潰したくない箇所まで塗り潰してしまう。塗り潰して欲しいところが塗りつぶされずに残ってしまう可能性が高いかなぁという印象です(これは私のハフ変換の知識が乏しいからかもしれません…)。

私は物体検出に詳しくないのですが、2. のところで「飛行機の軌跡」を物体検出するようにすれば、もしかしたら精度は上げられるかもしれないです。

返信する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です