
画像処理にチャレンジ(part 5): RGB分解
これまでの背景
part 4で止まっていた「画像処理にチャレンジ」であるが、久しぶりに進展があったので記録しておくことにする。
主な進展は、BMP形式におけるヘッダーファイルの読み取り関数の完成である。これにより、画像データ本体の加工、つまり画像処理が文字通り可能になった。
以前チャレンジしたのはJPEG形式のヘッダーファイルの読み取りであった。「EXIFデータが読めればいいな」程度の気持ちで始めたが、意外に複雑だったので途中で飽きてしまった。
そこで本来の目的である「画像処理」の方に興味が戻ったのであった。JPEGの画像データを直接扱うためには、圧縮のアルゴリズムを理解するか、圧縮の部分を担当してくれるライブラリー(例えばlibjpegなど)の仕様を理解する必要がある。どちらも面倒なので後回しにすることにして、まずは簡単な非圧縮系の画像ファイルを直接処理してみるのが(学習ステップ的には)適切であろう。ということで、非圧縮系の画像ファイルの典型例であるBMPに目をつけたというわけである。
ネットに溢れている一般利用の画像ファイルの中にBMPはもはや見かけない。しかし、科学的な目的で画像処理を行なっている人たち(人工衛星による地上探査やスペクトル分析とか、天体観測など)は、たぶんBMPやTIFFで画像処理をまだやっているのではないだろうか?そして発表する時は、それをPDFとかJPEGなどに変換してから公開しているような気がする。
ということで、自分でプログラムを組んで画像処理するときは非圧縮系の画像形式を扱い、その結果を発表する時はJPEGなどの圧縮形系の画像形式に変換してやればよい、ということになる。つまり、ImageMagickの出番ということである。やはり、このアプリは科学、技術系の研究者には欠かせないツールである。
まずは、BMP形式の画像データ読み取り関数を完成させる必要がある。今回は(なんとか)それを片付けることができた。
BMP形式の画像データのヘッダーの読み込み
BMPのヘッダー情報は2層に分かれていて、最初の14バイトがBMPFILEHEADERと呼ばれる基本的なヘッダーファイルである。これはどのタイプ(バージョン)のBMPファイルにも付与されている。最初に必ず読むべき14バイトである。
これをbmpReadHeaderという関数で読み取ることにしよう。
最初の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を買収したあの大金持ちも(以前は尊敬していたが今は)嫌いである。
ということで、BMP画像ファイルを読み込むプログラムのメイン関数は次のようになった。
#include <stdio.h>
#include <stdlib.h>
#define BMPHEADER_SIZE 14
int bmpReadHeader(char *);
int bmpReadImg(char *, int, int *);
int fourByteIntCalc(int, int *);
int main(int argc, char **argv){
int i, j, bmpHeaderSize, bmpFileSize;
int bmpWidth, bmpHeight;
int *bmpImg;
char *fileName;
if(argc != 2) return EXIT_FAILURE;
fileName = argv[1];
bmpFileSize = bmpReadHeader(fileName);
if(bmpFileSize <= 0) return EXIT_FAILURE;
else printf("FileSize: %d\n",bmpFileSize);
bmpImg = (int *)malloc( sizeof(int) * bmpFileSize );
bmpReadImg(fileName, bmpFileSize, bmpImg);
bmpHeaderSize = fourByteIntCalc(10, bmpImg);
bmpWidth = fourByteIntCalc(4+BMPHEADER_SIZE, bmpImg);
bmpHeight = fourByteIntCalc(8+BMPHEADER_SIZE, bmpImg);
printf("Header Size: %d \n", bmpHeaderSize);
printf("Width: %d Height: %d \n",bmpWidth, bmpHeight);
// bmpImgHandl() should be inserted here.
free(bmpImg);
return EXIT_SUCCESS;
}
効率的ではないとわかってはいるが、画像ファイルの読み出しは2つの関数、bmpReadHeader()とbmpReadImg())、に分割した。また、4バイトの16進表現を10進数の整数値に変換する関数fourByteIntCalc()も導入した。それぞれ、次のような形で実装できる。
ここから先は
¥ 500