読者です 読者をやめる 読者になる 読者になる

bussorenre Laboratory

bussorenre Laboratory

画像生成その1


※自分用メモです

どんどん無駄にカテゴリが増えていくぶっそれんれです。

一番大好きな言語、C++の衰えを肌で感じずにはいられない今日のこのごろ、今日はSoftware Engineering で使うかも知れない画像ジェネレートについてちょっと詳しくやってみようと思います。

コンピュータグラフィックの種類

1ピクセルごとに処理する方式(ペイントツールはこっち)と、ベクトルごとに処理する方式(3Dゲームはこっち)、の二つがあります。ベクトル方式のグラフィックライブラリで有名なのは、言わずと知れた、OpenGLDirectXのような、3D処理ライブラリですね。

私の得意分野も後者ですが、今回はあえて前者を使います。

方式

今回生成する画像イメージは、BMP形式です。画像形式と言えばJPGやPNG等が最近の主流ですが、BMPは1ドット1ドットを直接RGB(24ビット)で表せるのがメリットです。JPGやPNGは、圧縮をかけているためこのルールが当てはまりません。エンコード、デコードの処理が加わってしまうので、今回はそんな事をしないで済むようにBMP形式を選択しました。JPGやPNGへの圧縮は何かしらのライブラリを用いて圧縮をかけてください。

ただ先に言おう。windowsデフォのデータ形式は、いちいち処理がめんどくさい\(^o^)/www

BMPの構造

BMPは、ヘッダ部、情報部、データ部の3パートで構成されます。ヘッダ、情報部に関しては、windows.hで扱いやすいように構造体が定義されているので、それを使います。※WORDは2バイト長、DWORDは4バイト長のwindows.hで定義された変数です。Linux系で使うときは、DWORDをint WORDをshortにしてから扱うといいと思います。
ヘッダ部:BITMAPFILEHEADER 構造体(14 byte)

address メンバ 型(byte) 意味
0x0000 bfType WORD(2) 2バイトで"BM"という二文字が格納されている
0x0002 bfSize DWORD(4) 画像データのファイルサイズ
0x0006 bfReserved1 WORD(2) 予約領域1
0x0008 bfReserved2 WORD(2) 予約領域2
0x000a bfOffBits DWORD(4) 情報部修了までのオフセット(58固定)

特にいうことはありません。予約領域はそのまま0,0で埋めてください。


情報部: BITMAPINFOHEADER 構造体(36 byte) LONGの定義によって異なるが今回は(無理やり)LONG=4バイト固定で行く

address メンバ 型(byte) 意味
0x000e biSize DWORD(4) 情報部のサイズ。sizeofで定義すれば問題ない
0x0012 biWidth LONG(4) 画像の横幅
0x0016 biHeight LONG(4) 画像の建て幅
0x001a biPlanes WORD(2) プレーン?1固定。きっとBitMapが将来レイヤー構造に対応するのか
0x001c biBitCount WORD(2) 1ピクセル辺りのビット数。フルカラーなので24固定
0x001e biCompression DWORD(4) 圧縮形式。今回は向圧縮なので0
0x0022 biSizeImage DWORD(4) イメージのサイズ
0x0026 biXPelsPerMeter LONG(4) 解像度。よくわからないけど1固定
0x002a biYPelsPerMeter LONG(4) 解像度。よくわからないけど1固定
0x002e biClrUsed DWORD(4) パレットを使用しないので0
0x0032 biClrImportant DWORD(4) 意味不明。0固定

※LONGは一応今の仕様では4バイトですが、今後8バイトに変わるかもしれません。この辺はっきりしてほしい感じはありますねぇ……Cェ……

いまどきあまり2色、16色、256色とかあまり使わない上、今日は綺麗な画像を生成してこいとかいう、ちょっとした無茶ぶりを強いられている24ビットフルカラーのみ解説。

BMP形式の画像データは左から右、下から上。という風に、数学的なxy座標をとっています。(通常、コンピュータのxyの原点は左上。)なので、数学になれている人が扱う分には良いのですが、コンピュータグラフィックになれてる人が扱うと、上下逆向きに処理してしまう恐れがあります。

さらに曲者な仕組みがあります。1行(?)ごとにパディングするシステムです。1行の画像データが4の倍数バイトで納まらなかったら、0というデータを付随します。わけがわからないよ(←) 要するに、50×50の画像を生成すると、1行50×3バイト(24ビットカラー)×50行=7500バイトのデータで済みそうなのですが、1行150バイトは4の倍数ではありません。0x0000を付随して、1行152バイトにします。からの×50行。データ量が100バイトも増えてもったいないじゃないかっっ。
24ビットカラーの場合、RGBの順番で並んでいます。パディングにさえ気をつけて左下から順番通りに1ピクセルづつデータを並べていけば問題なさそうですね。


以上で、データ構造の解説は大体終わります。

これを踏まえてサンプルプログラムを作ります。

プログラムの方針

今回使用する言語はC++です。ちょっとクラスチックに、楽ーに楽ーに。
普段使っている開発環境はVisual Studio 2008 or 2010 ですが、gccも時々使うんで両方対応できるようにwindows.hはインクルードしません。全部ヘッダで定義します。1ドット1ドットずつ処理したいので、それ以外のややこしい部分はクラスでまかないます。

というわけで、これを踏まえてサンプルコードをちょっと作ってみました。

一応公開してますが、再利用とか考えないでくださいwww かなり適当で酷いソースコードです\(^o^)/
ソースコードに関して。特に特筆することはありません。めんどくさかったので、Managerクラスの中で描画テストやってますが、本来ならfriendクラスなりメソッドなりなんなりを用いて、外部から自由に描画出来る機能を用意すべきですが、まあそれは次の段階で。

というわけで、1ドットずつ、y=cos(x)の処理をかけてみました。1ドットずつ処理しているので、点線のような感じになってしまいます。
f:id:bussorenre:20110503181156j:image


本来なら、線の太さと、それに対するどっと間の補完的な処理が必要なのですが、今回はとりあえずbmpをいじれるようにする下地を作るのがメインだったので、今回はこれにておしまい\(^o^)/


ソースコードは続きを読むをクリックしてくださいまし。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

// 画像ジェネレートテスト

// windows . UNIX 両方対応できる工夫をここで行う。
typedef unsigned char       BYTE;            /* 1byte符号なし整数 */
typedef unsigned short      WORD;            /* 2byte符号なし整数 */
typedef unsigned long       DWORD;           /* 4byte符号なし整数 */
typedef long                LONG;            /* 4byte整数         */

#pragma pack(1)

typedef struct BMPFILEHEADER{
    char    bfType[2];
    DWORD	bfSize;
    WORD    bfReserved1;
    WORD    bfReserved2;
    DWORD	bfOffBits;
}BMPFILEHEADER;

typedef struct BMPINFOHEADER{
    DWORD	biSize;
    LONG    biWidth;
    LONG    biHeight;
    WORD    biPlanes;
    WORD    biBitCount;
    DWORD	biCompression;
    DWORD	biSizeImage;
    LONG    biXPelsPerMeter;
    LONG    biYPelsPerMeter;
    DWORD	biClrUsed;
    DWORD	biClrImportant;
}BMPINFOHEADER;

typedef struct RGBCOLOR{
    BYTE    rgbBlue;
    BYTE    rgbGreen;
    BYTE    rgbRed;
}RGBCOLOR;

#pragma pack()


// 管理用のクラス
class bmpManager {
private:
    BMPFILEHEADER	bmpHeader;
    BMPINFOHEADER	bmpInfo;

protected:
    unsigned int	nWidth;		// 横幅
    unsigned int	nHeight;	// 縦幅

    RGBCOLOR		*rgbImage;	// 画像データの本体	

public:
    
    // コントラクタ
    bmpManager()
    {
        memset(&bmpHeader,0,sizeof(BMPFILEHEADER));
        memset(&bmpInfo,0,sizeof(BMPINFOHEADER));
    };

    // デストラクタ
    ~bmpManager()
    {
        free(rgbImage);
    };

    // 画像データの生成
    int CreateBMPImage(unsigned int width,unsigned int height);

    // 画像データの吐き出し
    int OutputBMPImage(char *filename);

    // ヘッダの作成
    void MakeHeader();

    // 画像データをいじりまわす!!(ぁ
    void Generate();
};

/**--------------------------------------------------------------------------------
ビットマップイメージをメモリ上に作成する。作成されたBMPはとりあえず、真っ黒で生成される。

@param	width	横幅
@param	height	建て幅

@return	エラーコードを返す。基本的に1なら成功
--------------------------------------------------------------------------------*/
int bmpManager::CreateBMPImage(unsigned int width,unsigned int height)
{
    this->nHeight = height;
    this->nWidth = width;

    rgbImage = (RGBCOLOR*)malloc(sizeof(RGBCOLOR)*width*height);
    memset(rgbImage,0,sizeof(RGBCOLOR)*width*height);
    return 1;
}

/**--------------------------------------------------------------------------------
ファイルにビットマップイメージを出力する

@param	filename	出力先のファイル名

@return	エラーコードを返す。基本的に1なら成功
--------------------------------------------------------------------------------*/
int bmpManager::OutputBMPImage(char *filename)
{
    FILE *fp;
    fp = fopen(filename,"wb");
    if(fp==NULL){
        perror("Can't open the file.");
        return 0;
    }
    
    // ヘッダ部分を生成、書き込み
    MakeHeader();
    fwrite(&bmpHeader,sizeof(BMPFILEHEADER),1,fp);
    fwrite(&bmpInfo,sizeof(BMPINFOHEADER),1,fp);

    // データ部分を一行ずつ書き込み
    unsigned int i;
    char buf[4]="";
    for(i=0;i<nHeight;i++){
        fwrite(&rgbImage[i*nWidth],sizeof(RGBCOLOR),nWidth,fp);
        fwrite(buf,sizeof(char),nWidth%4,fp);
    }
    
    fclose(fp);
    return 1;
}

/**--------------------------------------------------------------------------------
ファイルヘッダ、情報部ヘッダを作成する
--------------------------------------------------------------------------------*/
void bmpManager::MakeHeader()
{
    int linedsize;

    // 1行のバイトサイズの計算がめんどくさい!w
    linedsize = (nWidth+3)/4;
    linedsize *=4;

    // file header
    bmpHeader.bfReserved1=0;
    bmpHeader.bfReserved2=0;
    bmpHeader.bfType[0]='B';
    bmpHeader.bfType[1]='M';
    bmpHeader.bfOffBits = sizeof(BMPFILEHEADER)+sizeof(BMPINFOHEADER);
    bmpHeader.bfSize = linedsize * nHeight * 3+bmpHeader.bfOffBits;
    
    // info header
    bmpInfo.biSize = sizeof(BMPINFOHEADER);
    bmpInfo.biWidth = nWidth;
    bmpInfo.biHeight = nHeight;
    bmpInfo.biPlanes = 1;
    bmpInfo.biBitCount = 24;
    bmpInfo.biCompression = 0;
    bmpInfo.biSizeImage = linedsize * nHeight *3;	// 1行あたりのバイト数×高さ×24bit
    bmpInfo.biXPelsPerMeter = 1;
    bmpInfo.biYPelsPerMeter = 1;
    bmpInfo.biClrUsed = 0;
    bmpInfo.biClrImportant = 0;

    return;
}

int main()
{
    int w,h;
    bmpManager		bmp;
    printf("横幅、縦幅を入力してください\n");
    scanf("%d",&w);
    scanf("%d",&h);

    bmp.CreateBMPImage(w,h);
    bmp.Generate();
    if(!bmp.OutputBMPImage("test.bmp"))
        printf("エラーエラー");
    
    return 0;
}


#define PI 3.1415926535897932384626433832795028841971

void bmpManager::Generate()
{
    // y = cos(x)で作ってみる
    int x,y;
    double t;

    DWORD i;
    for(i=0;i<nWidth*100;i++){
        t = i/100;

        y = (int)(nHeight/4*cos(PI/64*t))+nHeight/2;
        x = (int)(t+0.5);

        rgbImage[y*nWidth+x].rgbRed=255;
        rgbImage[y*nWidth+x].rgbBlue=255;
        rgbImage[y*nWidth+x].rgbGreen=255;

    }
    return;
}