今回はちょっとマニアックな話題です。C言語で1ビット画像を作る方法について解説します。
TIFFを扱いますので、プログラミングしたい方は事前に下記ページを読んで libtiff をインストールをし、使い方を知っておくと良いと思います。
【C言語】LibTIFFのインストールと使用方法・使用例Contents
1ビット画像とは
1ビット画像とは1ピクセルを1ビットで表現する画像のことを言います。一般的なカラー画像やモノクロ画像と比較しながら見ていくと分かりやすいと思います。
カラー画像
一般的なカラー画像は1ピクセルを24ビット(3バイト)で表現しています。赤色・緑色・青色の度合いをそれぞれ256段階で表し(つまり8ビットずつで表現)、その各々の度合いを調節することでいろんな色を表現することができます。
ピクセルについての詳細は下記ページで解説していますのでこちらも参考にしていただければと思います。
画像データの構造・画素・ビットマップデータについて解説スポンサーリンク
モノクロ画像
一般的なモノクロ画像は1ピクセルを8ビット(1バイト)で表現しています。白から黒からの色を256段階で表し(つまり8ビットで表現)、その度合いを調節することでいろんなグレーの色を表現することができます。
1ビット画像
これらのカラー画像やモノクロ画像に対し、1ビット画像では1ピクセルを1ビットで表現しています。1ビットで表現できるのは2つの情報だけですので、基本的に各ピクセルは「白」or「黒」のどちらかの色になります。
表現できるのが2つの値だけですので、1ビット画像は2値画像と呼ばれることもあります。また表現できるのが白と黒なので白黒画像とも呼ばれます。
1ピクセルあたりのデータサイズが小さいので、画像サイズも当然小さくなります。1ビット画像は割と印刷業界では使われるデータ形式のようです。また FAX などで送受信されるデータも1ビット画像のものが多いようです。
1ビット画像の作り方
まずは簡単に1ビット画像の作り方について解説し、その後1ビット画像を作成するC言語プログラムを紹介していこうと思います。
このページで解説する1ビット画像の作り方は、大きく分けると下記の4ステップで行います。
- カラー画像の読み込み
- カラー画像のモノクロ化
- モノクロ画像の1ビット化
- 1ビット画像の書き出し
スポンサーリンク
カラー画像の読み込み
まずは1ビット化するカラー画像の読み込みを行います。
この画像の読み込みに関しましては、他のページで解説していますのでこちらを参考にしていただければと思います。
・libjpeg を使用したJPEG ファイルの読み込み
【C言語】libjpegのインストールと使用方法・使用例・libpng を使用した PNG ファイルの読み込み
【C言語】libpngのインストールと使用方法・使用例・libtiff を使用した TIFF ファイルの読み込み
【C言語】LibTIFFのインストールと使用方法・使用例このページで紹介するプログラムでは「libtiff」を用いて TIFF ファイルを読み込み、それを1ビット画像に変換するものになります。
カラー画像のモノクロ化
1ビット化をする前にカラー画像を一旦モノクロ画像に変換します。
モノクロ化の仕方についても下記で解説していますので、必要に応じて目を通しておいていただけると後に紹介するプログラムが読みやすくなると思います。
【C言語】画像をグレースケール化モノクロ画像の1ビット化
ここからが1ビット画像作成本番で、いよいよモノクロ画像を1ビット化することで1ビット画像の作成を行います。
モノクロ画像の各ピクセルに対して下記を実行することで1ビット化することが可能です。
- ピクセルの値と閾値を比較
- 比較結果に応じてそのピクセルに対応するビットに値を設定
- 閾値以上の場合は “1” (白色)を設定
- 閾値未満の場合は “0” (黒色)を設定
1ビット化におけるポイントは2つです。
閾値の設定
1つ目は「閾値」です。1ビット画像で表現できるのは “1” or “0” の2つのみです。ですので、1ビット化する場合は、モノクロ画像の各ピクセルが取りうる0から255の値を、ある値以上のピクセルは “1” に、ある値未満のピクセルは “0” に置き換えることで2つの値のみから表現されるデータに変換します。
その「ある値」が閾値です。
この閾値は画像に合わせて上手く設定してやる必要があります。
例えば下記のモノクロ画像を2値化する場合、閾値によって1ビット画像がどのように変化するかを見てみましょう。
閾値を「100」に設定した場合は下のようになり、猫はぼんやり見えますがハンドルなんかは全て白色になって見えなくなってますね。
閾値を「150」にした場合は下のようになり、今度はハンドルが見えるようになり、猫もクッキリ描画されるようになりました。
閾値を「200」にした場合は下のようになり、今度は黒が多すぎてなんとなーく猫がいるのは分かりますが目のあたりが潰れてしまっています。
このように閾値の設定により作成される画像が大きく変わりますので、閾値は画像を作成しながら調節するのが良いです。
ビット演算
もう一つのポイントはビット演算です。
モノクロ画像の各ピクセルは8ビットで表現されているので、1バイトに1ピクセル分のデータが格納されますが、1ビット画像のピクセルは1ビットですので、1バイトに8ピクセル分のデータを詰め込むことになります。
モノクロ画像を1ビット化する場合、閾値との比較によって定まった値 “1” or “0” をビット単位でバイトデータに詰め込んでいきます。
バイトデータに詰め込んでいくイメージを図示すると下の図のようになります。
このビット単位の処理を行うためにはビット演算が必要になります。ビット演算に自信がない方はまず下のページを読んでビット演算を理解しておくと良いと思います。
C言語のビット演算(論理演算)について解説例えばポインタ data が指すアドレスの第5ビットに “1” を格納するのであれば下記のようにビット演算を行います。
*data = *data | (1 << 5);
逆に “0” を詰め込むのであれば下記のようにビット演算を行います。
*data = *data & (~(1 << 5));
スポンサーリンク
1ビット画像の書き出し
1ビットの画像データを作成できれば、次は作成した画像データをエンコードしてファイルとして書き出す処理を行います。
ポイントになるのが使用するエンコード形式(圧縮形式)です。有名なエンコード形式でも1ビット画像に対応していない場合もあります。
このページでは CCITT Group 4 圧縮形式を用いて1ビット画像をエンコードします。FAX の送受信データの圧縮でよく用いられる圧縮方法です。
あまり聞きなれない画像形式だと思いますが、「libtiff」を用いれば、1ビット画像データさえ用意しておけば、圧縮形式として CCITT Group 4 を選択するだけで1ビット画像をエンコードすることが可能ですし、それを CCITT Group 4 で圧縮した TIFF ファイルとして書き出すことができます。
この形式であれば、Mac OSX のプレビューでも開くことができますし、Windows のプレビューでも開くことができます。なので圧縮結果を簡単に確認することが可能です。
1ビットの TIFF は “1bitTIFF” や “2値 TIFF”などと呼ばれることもあります。
1ビット画像を作成するプログラム
まずはプログラムの全体を紹介します。
#include "myTiff.h"
#include <string.h>
/* 2値化するときの閾値 */
#define TH 100
int writeOneBitTiff(unsigned char *, unsigned int, unsigned int, char *);
/* 1ビットTIFFをファイルに書き出す関数 */
/* widthは8の倍数である必要あり */
int writeOneBitTiff(unsigned char *data, unsigned int width, unsigned int height, char *filename){
TIFF *tiff;
unsigned int dataSize;
/**** 1ビット画像の書き出しここから ****/
/* TIFFファイルを開く */
tiff = TIFFOpen(filename, "w");
if(tiff == NULL){
printf("TIFFが開けません\n");
return -1;
}
/* TIFFにタグを設定 */
/* 画像の横サイズ */
TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, width);
/* 画像の縦サイズ */
TIFFSetField(tiff, TIFFTAG_IMAGELENGTH, height);
/* 1色を表現するビット数 */
TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 1);
/* ピクセルあたりの色数 */
TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, 1);
/* 圧縮形式 */
TIFFSetField(tiff, TIFFTAG_COMPRESSION, COMPRESSION_CCITTFAX4);
/* 0の色(MINISBLACKだと0を黒で表現)*/
TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
/* データサイズはピクセル数 / 8 */
dataSize = width * height / 8;
printf("%d\n", dataSize);
/* 開いたファイルにエンコードしたデータを書き込み */
if(TIFFWriteEncodedStrip(tiff, 0, data, dataSize) == 0){
printf("TIFFに書き込めません\n");
return -1;
}
/* TIFFを閉じる */
TIFFClose(tiff);
/**** 1ビット画像の書き出しここまで ****/
return 0;
}
int main(int argc, char *argv[]){
BITMAPDATA_t bitmap, grayBitmap;
int m, n;
unsigned char y;
unsigned char *oneBitData;
unsigned int dataSize;
unsigned int byte;
unsigned char bit;
unsigned int alignedWidth;
if(argc != 2){
printf("ファイル名が指定されていません\n");
return -1;
}
/**** カラー画像の読み込みここから ****/
/* TIFFファイルの読み込み */
if(tiffFileReadDecode(&bitmap, argv[1]) == -1){
printf("tiffFileReadDecode error\n");
return -1;
}
/**** カラー画像の読み込みここまで ****/
/* グレースケール用の画像データを作成 */
grayBitmap.width = bitmap.width;
grayBitmap.height = bitmap.height;
grayBitmap.ch = 1;
grayBitmap.data = (unsigned char*)malloc(sizeof(unsigned char) * grayBitmap.height * grayBitmap.width * grayBitmap.ch);
if(grayBitmap.data == NULL){
printf("メモリが足りません\n");
freeBitmapData(&bitmap);
return -1;
}
/**** カラー画像のモノクロ化ここから ****/
/* グレースケールに変換したデータをgrayBitmapに格納 */
for(n = 0; n < bitmap.height; n++){
for(m = 0; m < bitmap.width; m++){
y = 0.299 * bitmap.data[bitmap.ch * (m + n * bitmap.width) + 0]
+ 0.587 * bitmap.data[bitmap.ch * (m + n * bitmap.width) + 1]
+ 0.114 * bitmap.data[bitmap.ch * (m + n * bitmap.width) + 2];
grayBitmap.data[m + n * bitmap.width] = y;
}
}
/**** カラー画像のモノクロ化ここまで ****/
/* 元画像は不要なので解放 */
freeBitmapData(&bitmap);
/* 扱いやすいように画像の横サイズを8の倍数に切り上げ */
alignedWidth = ((grayBitmap.width + 7) / 8) * 8;
/* データサイズはピクセル数 / 8 */
dataSize = grayBitmap.height * alignedWidth / 8;
/* 1ビット画像データを格納するためのメモリを確保 */
oneBitData = (unsigned char*)malloc(sizeof(unsigned char) * dataSize);
if(oneBitData == NULL){
printf("メモリが足りません\n");
freeBitmapData(&grayBitmap);
return -1;
}
/* 全ピクセルを一旦1(黒色)で初期化 */
/* 0xFF = 0b11111111 */
memset(oneBitData, 0xFF, dataSize);
/**** モノクロ画像の1ビット化ここから ****/
/* 最初にビット値を格納するビットを最上位ビットに設定 */
bit = 7;
byte = 0;
/* 全画素を1ビット化 */
for(n = 0; n < grayBitmap.height; n++){
for(m = 0; m < grayBitmap.width; m++){
if(grayBitmap.data[m + n * grayBitmap.width] >= TH){
/* (m, n)画素の値が閾値以上の場合
(m, n)画素に対応するアドレスの
第bitビットを"1"に設定 */
oneBitData[byte] =
oneBitData[byte] | (1 << bit);
} else {
/* (m, n)画素の値が閾値未満の場合
(m, n)画素に対応するアドレスの
第bitビットを"0"に設定 */
oneBitData[byte] =
oneBitData[byte] & (~(1 << bit));
}
if(bit == 0 || m == grayBitmap.width - 1){
/* 1バイト分の値を格納し終わったので
次のバイトの最上位ビットに移動 */
byte++;
bit = 7;
} else {
/* まだ1バイト分値を格納し終わっていない場合
次の下位ビットに移動 */
bit--;
}
}
}
/**** モノクロ画像の1ビット化ここまで ****/
/* 1ビット画像をTIFF形式で書き出し */
writeOneBitTiff(oneBitData, alignedWidth, grayBitmap.height, "out.tiff");
/* データを解放 */
freeBitmapData(&grayBitmap);
free(oneBitData);
return 0;
}
プログラムのコンパイル・実行方法
続いてプログラムのコンパイル・実行を行うために事前に準備しておくものとコンパイル・実行方法の解説を行います。
事前に準備しておくもの
上記プログラムを変更せずにそのまま実行するために事前に3つのものが必要です。
①libtiff
libtiff を使用して TIFF ファイルの読み込みを行なっていますので、下記ページを参照して事前に libtiff をインストールしておいてください。
【C言語】LibTIFFのインストールと使用方法・使用例②myTiff.c と myTiff.h
TIFF ファイルの読み込みを行う私の自作関数 tiffFileReadDecode 関数は myTiff.c と myTiff.h で定義しています。こちらも上記の libtiff 解説ページで公開していますので、コピペして myTiff.c と myTiff.h を作成しておいてください。
③TIFF ファイル
1ビット化を行う TIFF ファイルを準備しておいてください。準備する TIFF ファイルは RGB カラーのアルファチャンネルなしの TIFF ファイルにしてください。
コンパイル・実行方法
コンソール等のコマンドラインで gcc を用いてコンパイルする場合は下記の3つのコマンドを実行すればコンパイルすることができます。
gcc main.c myTiff.c -ltiff -o main.exe
これにより main.c と myTiff.c の2つをコンパイルした結果と “libtiff” をリンクして実行ファイル “main.exe” が作成されます。ポイントは libtiff をリンクするために “-ltiff” をつけることです。
実行は下記のように、作成した “main.exe” を実行すれば良いです。引数に用意した TIFF ファイルを指定してください。
./main.exe [TIFFファイル名]
これにより指定した TIFF ファイルを1ビット化した “out.tiff” が作成されます。
スポンサーリンク
プログラムの解説
基本的に1ビット画像の作り方で解説した下記の4つを行っているプログラムになります。
- カラー画像の読み込み
- カラー画像のモノクロ化
- モノクロ画像の1ビット化
- 1ビット画像の書き出し
ソースコード中で「/**** xxxxここから ****/」と「/**** xxxxここまで ****/」とコメントを書いていますので、その部分と上記4つの解説を照らし合わせて読んでいただけると処理の大枠は掴めると思います。
カラー画像の読み込み
tiffFileReadDecode 関数でカラー画像を libtiff で TIFF を読み込んでデコードしています。この関数については下記のページで紹介していますので興味がある方は読んでみてください。
【C言語】LibTIFFのインストールと使用方法・使用例カラー画像のモノクロ化
こちらに関しては下の「C言語で画像をモノクロ・グレースケール化」のページで解説していますので、詳細を知りたい方はこちらを参考にしてください。
【C言語】画像をグレースケール化モノクロ画像の1ビット化
ポイントは3つです。
1つ目は横幅の調整です。C言語で扱えるデータの型は基本的にバイト単位です(例えば char 型もサイズは1バイトですよね)。ビット単位で扱える基本的なデータの型はありません。ですので、1ビット画像の1ライン分をバイト単位で扱えるように、画像の横幅を8の倍数に調整し、調整後の横幅で1ビット画像のデータを確保するメモリを確保しています。
/* 扱いやすいように画像の横サイズを8の倍数に切り上げ */
alignedWidth = ((grayBitmap.width + 7) / 8) * 8;
/* データサイズはピクセル数 / 8 */
dataSize = grayBitmap.height * alignedWidth / 8;
/* 1ビット画像データを格納するためのメモリを確保 */
oneBitData = (unsigned char*)malloc(sizeof(unsigned char) *
で、基本的にモノクロ画像のデータ(先頭アドレスは graybitmap.data)を原点から横方向に対して各ピクセルの値が閾値以上か閾値未満かに応じて、1ビット画像データ(先頭アドレスは oneBitData)に値を格納していっています。この値の格納はビット演算により実行しています。
if(grayBitmap.data[m + n * grayBitmap.width] >= TH){
/* (m, n)画素の値が閾値以上の場合
(m, n)画素に対応するアドレスの
第bitビットを"1"に設定 */
oneBitData[byte] =
oneBitData[byte] | (1 << bit);
} else {
/* (m, n)画素の値が閾値未満の場合
(m, n)画素に対応するアドレスの
第bitビットを"0"に設定 */
oneBitData[byte] =
oneBitData[byte] & (~(1 << bit));
}
このビット演算が2つ目のポイントですね。ビット演算について詳しくしたい方は下記ページを是非読んでみてください。
C言語のビット演算(論理演算)について解説3つ目のポイントはアドレスやビットの変更です。ソースコード中における “byte” は oneBitData の先頭から何バイト目かのデータに値を格納するか、 “bit” はそのデータの何ビット目かにデータを格納するかを制御する変数になっています。
oneBitData には1つのバイトに対して8ピクセル分のデータを格納していく訳ですから、8ピクセル分のデータを格納した時に byte を1増やすように制御する必要があります。このプログラムでは最上位ビット(第7ビット)から順に最下位ビット(第0ビット)まで順に値を格納していくようにしていますので、bit が最下位ビットを表す “0” の状態で値を格納し終わった時に、byte を次のバイトに移動し(byte++)、bit を最上位ビットを表す “7” に戻す処理を行っています。
最下位ビットでない場合は単に bit を1減らす処理を行います。これらを行っているのが下記部分になります。
if(bit == 0 || m == grayBitmap.width - 1){
/* 1バイト分の値を格納し終わったので
次のバイトの最上位ビットに移動 */
byte++;
bit = 7;
} else {
/* まだ1バイト分値を格納し終わっていない場合
次の下位ビットに移動 */
bit--;
}
これらをモノクロ画像の全ピクセルに対して処理してやれば、oneBitData に graybitmap.data に格納されたモノクロ画像データを1ビット化したデータが格納されます。
1ビット画像の書き出し
最後に行うのは1ビット画像データのエンコードとファイル書き出しです。これらの処理は writeOneBitTiff 関数で行なっています。
この関数では下記の四つの処理を libtiff が提供する関数を用いて実行しています。
- TIFFを開く(TIFFOpen)
- タグを設定(TIFFSetField)
- エンコードとファイル書き込み(TIFFWriteEncodedStrip)
- TIFFファイルを閉じる(TIFFClose)
ポイントは「タグ設定」です。TIFF ファイルは大きく分けると「(エンコードされた)画像データそのもの」と「画像の情報を表すタグ」の二つから構成されます。
この「タグ」には画像のサイズ・各ピクセルのビット数・エンコード形式などの画像の情報が格納されています。プレビューなどの画像を表示するアプリはこの「タグ」の情報から画像がどのようなものかを調べ、その情報に基づいてデータをデコードして表示しているというわけです。
libtiff ではこの「タグ」を TIFFSetField 関数で設定することができます。どのようなタグを設定しているかはソースコード中のコメントを見ていただければ理解していただけると思います。
特に下記の2つは1ビット画像を libtiff に入力する時に必要な設定になります。
/* 1色を表現するビット数 */
TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 1);
/* ピクセルあたりの色数 */
TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, 1);
また下記ではエンコード形式を設定しており、エンコード形式に1ビット画像の書き出しで紹介したCCITT Group 4 を指定しています。
/* 圧縮形式 */
TIFFSetField(tiff, TIFFTAG_COMPRESSION, COMPRESSION_CCITTFAX4);
そして TIFFWriteEncodedStrip 関数に、そのタグを設定した tiff と1ビット画像のデータサイズと先頭アドレスを指定して実行することで、指定したデータがエンコードされてファイルとして書き出されることになります。
/* 開いたファイルにエンコードしたデータを書き込み */
if(TIFFWriteEncodedStrip(tiff, 0, data, dataSize) == 0){
printf("TIFFに書き込めません\n");
return -1;
}
libtiff が提供している関数の詳細は下記ページから調べることができますので、ここで使った関数について詳しく知りたい方は下記を参照していただければと思います。
参考 Man Pages - LibTIFFlibtiff.orgまとめ
このページではまず1ビット画像について解説し、続いてその1ビット画像の作り方と1ビット画像を作成するプログラムについて解説しました。
なかなか1ビット画像を扱う機会は無いかもしれませんが、1ビット画像を作成するプログラムを作ってみるとかなりプログラミングの力が付きますので一度自分で作ってみても良いと思います。難易度も高いですし、画像として結果が確認できますので作成出来たときは達成感を感じられると思います。