複天一流:どんな手を使ってでも問題解決を図るブログ

宮本武蔵の五輪書の教えに従い、どんな手を使ってでも問題解決を図るブログです(特に、科学、数学、工学の問題についてですが)

画像処理にチャレンジ(part 5): RGB分解

これまでの背景

part 4で止まっていた「画像処理にチャレンジ」であるが、久しぶりに進展があったので記録しておくことにする。

主な進展は、BMP形式におけるヘッダーファイルの読み取り関数の完成である。これにより、画像データ本体の加工、つまり画像処理が文字通り可能になった。

以前チャレンジしたのはJPEG形式のヘッダーファイルの読み取りであった。「EXIFデータが読めればいいな」程度の気持ちで始めたが、意外に複雑だったので途中で飽きてしまった。

そこで本来の目的である「画像処理」の方に興味が戻ったのであった。JPEGの画像データを直接扱うためには、圧縮のアルゴリズムを理解するか、圧縮の部分を担当してくれるライブラリー(例えばlibjpegなど)の仕様を理解する必要がある。どちらも面倒なので後回しにすることにして、まずは簡単な非圧縮系の画像ファイルを直接処理してみるのが(学習ステップ的には)適切であろう。ということで、非圧縮系の画像ファイルの典型例であるBMPに目をつけたというわけである。

ネットに溢れている一般利用の画像ファイルの中にBMPはもはや見かけない。しかし、科学的な目的で画像処理を行なっている人たち(人工衛星による地上探査やスペクトル分析とか、天体観測など)は、たぶんBMPTIFFで画像処理をまだやっているのではないだろうか?そして発表する時は、それをPDFとかJPEGなどに変換してから公開しているような気がする。

ということで、自分でプログラムを組んで画像処理するときは非圧縮系の画像形式を扱い、その結果を発表する時はJPEGなどの圧縮形系の画像形式に変換してやればよい、ということになる。つまり、ImageMagickの出番ということである。やはり、このアプリは科学、技術系の研究者には欠かせないツールである。

まずは、BMP形式の画像データ読み取り関数を完成させる必要がある。今回は(なんとか)それを片付けることができた。

BMP形式の画像データのヘッダーの読み込み

BMPのヘッダー情報は2層に分かれていて、最初の14バイトがBMPFILEHEADERと呼ばれる基本的なヘッダーファイルである。これはどのタイプ(バージョン)のBMPファイルにも付与されている。最初に必ず読むべき14バイトである。

これをbmpReadHeaderという関数で読み取ることにしよう(関数の詳細はnoteを参照)。

最初の2バイトは「マジックナンバー」と呼ばれるもので「BM」と書かれている。逆に、これが書いてあれば、BMPファイルである。ファイル形式の識別に利用する。

次の4バイトがファイルサイズである。Unixなら「ls -l」というコマンドで表示されるファイルの大きさを表す数で単位は「バイト」である。

更なる次の4バイトは歴史の淘汰によって利用されなくなった「予約領域」と呼ばれるダミー情報である。ここは0が入っている場合が多いが、利用しないのでどんな値にしても基本的には構わないだろう。

最後の4バイトが画像データが始まる「アドレス」のようなものである。言い換えれば、この値はヘッダー全体(2層分)の大きさにも等しい。たとえば、この値が54バイトならば、bmp画像ファイルの54バイト目から画像データが始まり、それより前はヘッダー情報ということになるが、同時に、最初のBMPFILEHEADERが14バイトなので、2層目のヘッダーの大きさが40バイトということも言える(40バイトのサイズを持った2層目のヘッダーはBMPINFOHEADERと呼ばれる)。

ということで、最初に14バイトの「基本」ヘッダーを読み込み、その情報を使って、ヘッダー部分と画像部分にデータを分割し、画像処理は後者のみに対して行うのが基本的なアルゴリズムということになろう。読み取りの構造(のスケッチ)としては、プログラムは次のような感じになるだろう。

#define BMPFILEHEADER 14
int main(){
 int bmpFileSize = bmpReadHeader(BMPFILEHEADER);
 int *bmpImg = bmpReadImg(bmpFileSize);

 bmpImgHandle(bmpImg);
}

もちろん、このままではコンパイルエラーになる(変数の宣言とか細かいところを省いているので)。大事な点は、Headerのデータをどうやってmain()に持ってくるかだが、その実装方法は人によって違うだろう。よく使う手としてはmalloc()を使って領域確保する方式だろう。その場合は、最初のbmpReadHeader()関数によって手に入れたファイルサイズbmpFileSizeを使って、

bmpImg = (int *)malloc(sizeof(int) * bmpFileSize);

とやるのが普通のアプローチだと思う。malloc()はvoid関数で定義されているので、画像データの8ビット階調表現として利用するなら整数型にキャストしておいた方が安全だろう。また、確保する領域は整数型(4バイト=sizeof(int))なので、ファイルサイズにその値をかけておかないと確保されたメモリ領域から画像データが溢れてしまい、segmentation faultかなにかのエラーを引き起こすだろう。

#define BMPFILEHEADER 14
int main(){
  int bmpFileSize = bmpReadHeader(BMPFILEHEADER);
   int *bmpImg = (int *)malloc(sizeof(int) * bmpFileSize);
  int bmpImg = bmpReadImg(bmpFileSize);

   bmpImgHandle(bmpImg);
}

気に食わない点が一つある。それはbmpReadHeader()bmpReadImg()という2つの関数を導入して、画像ファイルに2回アクセスしている点である。 なんとか節約して一つにまとめたいと思ったのだが、うまくいかなかった。なんとかやれないことはないのだが、そうするとプログラムが複雑になってしまうのだ。簡潔に、かつ集約した形でファイルアクセスできるといいのだが、今回はそこにこだわっている時間はないので、このまま先を進むことにした。

ちゃんとコンパイルできるプログラムは今回もnoteに載せることにする。

これはAIに内容を食われないようにするためである(もっとも、AIはすでにこの程度のプログラム技能は身につけているとは思うが、飲み込んで「ぺっ」と吐き出されるのですら嫌悪感がある)。個人的に、アメリカの新しい大統領とその取り巻きの商人たちに対して、私は以前よりあまり良い感情をもっていないのであるが、先ほどのニュースで、この大統領に媚びるAI系の「よいしょ野郎」たちが巨額の投資を受けられることになったと報じられていた。その会見の場に大統領に並んで立っていたのがOpenAIのCEOであった(そういえば、なぜかソフトバンクの会長も隣にいた)。ちなみに、この大統領に早い段階から媚びていた、Twitterを買収したあの大金持ちも(以前は尊敬していたが今は)嫌いである。

www3.nhk.or.jp

これでBMPファイルを読み込めるようになったので、いろいろと処理して遊んでみよう。まずは、以前ヘッダーだけ読み込んで喜んでいた大谷選手の画像をRGB分解してみたい。

BMPファイルは、白黒画像とカラー画像の両方を扱うことができるようだが、最終目的は天体写真の分析であるから、カラー画像の処理ができるようになっていた方がいいだろう。人間の目の特徴を生かし、色データはRGBの三色に分解されて表現される。それぞれが8ビット表現、つまり256階調で表される。つまり3バイトでピクセル一つを表すのである。座標情報は、ピクセルデータの並び方によって記述される。したがって、画像ヘッダーに含まれる縦と横のサイズの情報は重要な役割を持っているので、今回のプログラムでも読み取るようにしてある(詳しくはnoteに書いたプログラムを参照)。

画像データは本来2次元座標に対する関数F(x,y)であるが、BMP画像の場合、画像の横サイズbmpWidthがわかると1次元表現indexによって次のように表すことができる。 $$ \verb+index+ = x + \verb+bmpWidth+ \times y $$

カラー画像の場合は、3バイトあることを考慮するともう少し複雑な公式になるが、その修正はそれほど難しくない。 ただ、RGB分解する場合には修正公式の詳細は知っている必要はなく、$\verb+index+ \% 3 $の値だけを気にしていればよい。0,1,2の3つのパターンがあり、そのうちどれかを選ぶと、その数に対応した色だけが記録されることになる。BMPの場合は青、緑、赤の順番でデータは格納されているようであるが、それを確かめてみよう。

ファイル出力はバイナリーファイルとなるのでファイルポインタ$\verb+fp+$の定義などを書き込む必要があるが、その部分は省略し(というかnoteに書くことにして)、$\verb+bmpImgHndl()+$の部分を次のようにして、main関数に差し込んでみる。

for(index = 0; index < ImgDataSize; index++){
  if ( (index % 3) == COLOR_ID ) 
     fputc(bmpImg[index], fp);
  else
     fputc(0, fp);
}

この関数はvoid型でもいいし、成功したら整数値1を返すint型にしてもいいだろう。マクロ定義で$\verb+COLOR_ID+$を最初は0に指定して、コンパイル、実行してみる。予想としては「真っ青な大谷選手」が出てくるはずである。

(noteにおいたMakefileを使ってコンパイルした場合)次のように実行する。

% bmpRead ohtani.bmp

test_out.bmpというBMP画像ファイルが出力される。開いてみると次のようになって、予想通りの結果となっていた!

Blue成分だけを書き出した大谷の画像(JPGに変換してある)

座標の情報も使ったり、色データを組み合わせてやると、もうすこし面白い処理ができる。たとえば、こんな感じである。

右上だけ未処理、残りの領域では青の情報を抜いてから緑と赤の情報を階調反転処理して作った画像