Rustは実行効率や安全性を重視した人気のプログラミング言語ですが、難しいと言われることもあります。本連載ではいろいろな有名アルゴリズムを解くことでRustに慣れていきます。今回は、画像ファイルの生成方法について見てみましょう。

  • ゼロから市松模様の画像を生成するプログラムを作ってみよう

    ゼロから市松模様の画像を生成するプログラムを作ってみよう

画像ファイルとは何か

そもそも、画像ファイルとは何でしょうか。コンピューターのモニターは小さな点(ピクセル)の集まりです。それで、画像ファイルを表現する場合は、ピクセルの並びをそのままデータとして表現した「ビットマップ画像(ラスタ画像)」と、図形をどのように描画するのか座標情報で表現する「ベクタ画像」の二種類が代表的です。今回は、Rustで単純にピクセル情報をファイルに保存すれば良いビットマップ画像を作成してみましょう。

  • 画像の一部を拡大するとピクセルデータであることが分かる

    画像の一部を拡大するとピクセルデータであることが分かる

ビットマップ画像ファイルの構造

ビットマップ画像について簡単に分かったところで、実際のファイル構造について見ていきましょう。基本的には、画像の各ピクセルの色番号を左上から右下に並べたデータに加えて、画像情報を表すヘッダ情報のデータをくっつけたものです。ヘッダ情報はビットマップファイルであることを示すファイルヘッダと、画像に関する情報をまとめた情報ヘッダから構成されます。次の図のような構造となっています。

  • ビットマップ画像のファイル構造

    ビットマップ画像のファイル構造

画像の各ピクセルは、光の三原色である赤緑青(RGB)の色の割合を指定したものとなっています。例えば、24ビットTrue Colorでは赤緑青をそれぞれ8ビット(0-255の範囲)で指定して色を表現します。なお、この時、青・緑・赤の順番で指定します。

なお、Webでよく見かける、PNGやJPEG画像は画像データを圧縮したものですが、基本的にビットマップ画像は圧縮が不要な単純な画像データです。

ビットマップの具体的な構造

それでは、より具体的にファイル構造を確認してみましょう。ビットマップファイルのヘッダ情報は、次のような構造となっています。

  • ビットマップ画像のヘッダ情報

    ビットマップ画像のヘッダ情報

実際の画像データに加えて、上記の表の通りのバイト数でファイルに書き込めばビットマップ画像ファイルができあがります。

バイトオーダーについて

なお、ビットマップのヘッダを書き込む際、数値はすべて「リトルエンディアン」にて書き込む必要があります。これは数値データをメモリに書き込む際に、どのような順番で書き込むかの方式です。「ビッグエンディアン」と「リトルエンディアン」があり、「バイトオーダー(エンディアン)」と呼びます。

例えば、32ビット整数の0x12345678を書き込む際に、ビッグエンディアンであれば、[0x12, 0x34, 0x56, 0x78]のようにしますが、リトルエンディアンでは、[0x78, 0x56, 0x34, 0x12]のように書き込みます。

  • バイトオーダーについて

    バイトオーダーについて

画像ファイルを作成してみよう

以上、ここまでの知識があれば、ビットマップ画像をファイルに保存することができるでしょう。それでは、Rustでビットマップ画像を書き出すプログラムを作ってみましょう。

なお、今回からRustのプロジェクト管理ツールである、Cargoを使ってみましょう。プロジェクトを作成するには、コマンドラインで次のようなコマンドを実行します。

cargo init

そして、src/main.rsを次のように書き換えましょう。こちらからもコピーできます。

use std::io::Write;
fn main() { // メイン処理 --- (*1)
    let data = make_bitmap(8, 8); // 画像を作成
    let mut f = std::fs::File::create("test.bmp").unwrap(); // ファイル生成
    f.write(&data).unwrap(); // ファイルに書き出す
    println!("画像を書き出しました。");
}
// ビットマップ画像データを作成する --- (*2)
fn make_bitmap(width: u32, height: u32) -> Vec<u8> {
    let mut data = vec![];
    let image_size = width * height * 3; // サイズ計算 --- (*3)
    let filesize = 14 + 40 + image_size; //
    // ファイルヘッダを書き込む --- (*4)
    data.push('B' as u8); data.push('M' as u8);
    write_u32(&mut data, filesize); // ファイルサイズ
    write_u32(&mut data, 0); // 0
    write_u32(&mut data, 0x36); // 先頭から画像データまでのオフセット
    // 情報ヘッダを書き込む --- (*5)
    write_u32(&mut data, 40); // ヘッダサイズ
    write_u32(&mut data, width); // 幅
    write_u32(&mut data, height); // 高さ
    write_u16(&mut data, 1); // 1
    write_u16(&mut data, 24); // 色ビット数
    write_u32(&mut data, 0); // 圧縮形式
    write_u32(&mut data, image_size); // 画像データサイズ
    write_u32(&mut data, 0); // 水平解像度(0も可/96dpi=3780)
    write_u32(&mut data, 0); // 垂直解像度(0も可/96dpi=3780)
    write_u32(&mut data, 0); // パレット数
    write_u32(&mut data, 0); // 重要色数(0でも良い)
    // 画像データを書き込む --- (*6)
    for y in 0..height {
        for x in 0..width {
            let color = if (x+y) % 2 == 0 { 0xFF0000 } else { 0xFFFFFF };
            write_rgb(&mut data, color);
        }
    }
    data
}
fn write_u16(data: &mut Vec<u8>, v: u16) { // u16を書き込む --- (*7)
    for i in v.to_le_bytes() { data.push(i); }
}
fn write_u32(data: &mut Vec<u8>, v: u32) { // u32を書き込む
    for i in v.to_le_bytes() { data.push(i); }
}
fn write_rgb(data: &mut Vec<u8>, rgb: u32) { // RGBを書き込む --- (*8)
    data.push((rgb >>  0 & 0xFF) as u8); // Blue
    data.push((rgb >>  8 & 0xFF) as u8); // Green
    data.push((rgb >> 16 & 0xFF) as u8); // Red
}

書き換えたら、次のコマンドを実行します。これにより、コンパイルが行われてプログラムが実行されます。

cargo run

すると、8x8のサイズの「test.bmp」という画像ファイル(ビットマップ画像)が生成されます。せっかくなので、市松模様を生成するようにしてみました。

  • プログラムを実行して作成されたビットマップ画像

    プログラムを実行して作成されたビットマップ画像

それでは、プログラムを確認してみましょう。プログラムは大きく分けて、二つの部分に分けられます。メモリ内に画像を生成する部分(関数make_bitmap)と、メモリ内のデータをファイルに保存する部分(関数main)です。

(*1)では、ビットマップ画像を生成して、ファイルに書き込む関数mainを記述します。「test.bmp」というファイルを保存します。

(*2)の関数make_bitmapを定義します。この関数は、ビットマップ画像をVec<u8>型で生成して返すものです。

(*3)では、まず画像データ部分のデータサイズを計算して、変数image_sizeに代入します。今回、24ビットTrue Color画像を書き出しますので、画像の幅×画像の高さ×3を計算することでデータサイズが計算できます。次に、ファイルサイズ(変数filesze)を計算しますが、これはimage_sizeにヘッダのサイズ(ファイルヘッダ14バイト、情報ヘッダ40バイト)を足したものです。

(*4)ではファイルヘッダを、(*5)では情報ヘッダを、(*6)では画像データを書き込みます。その際、(*7)以降で定義している関数write_u32とwrite_u16を指定して書き込みます。

なお、(*4)のファイルヘッダで重要なのはファイルサイズです。これは、ファイルの先頭から末尾までのファイルサイズで、これが間違っていると、ファイルが壊れていることになってしまうので注意が必要です。そして、(*5)で必要なのは、画像の幅と高さです。

(*6)の画像データは、市松模様になるように座標(x, y)のxとyを足して2で割った余りを利用して、書き込む色データを決定しています。

(*7)では、符号なし16ビット整数u16型と、符号なし32ビット整数u32型をVec<u8>に書き込む関数を定義しています。ここでは、リトルエンディアンで書き込むため、to_le_bytesメソッドを呼び出して、バイトデータを得てfor文でVec<u8>に追加します。なお、Vec型には、writeメソッドが用意されているので、直接writeメソッドを呼び出すこともできます。ここでは、何が起きているか処理が分かりやすくなるように、for文にしました。

(*8)では、HTMLなどで一般的な色コードの指定方法(RGB)を指定したカラーコードを、BGRの順番でVec<u8>に書き込む関数write_rgbを定義します。

まとめ

以上、今回は、ビットマップ画像ファイルを書き込むプログラムを作ってみました。50行のプログラムで画像ファイルが作成できました。基本的なRustの文法さえ押さえていれば、特に難しい点はないでしょう。

ただし、画像ファイルを作成する場合には、画像フォーマットについても知っている必要がありますし、バイナリデータを扱う場合には、バイトオーダーについて学んだり、どのようにバイナリデータを作成するのかを学んだりする必要があります。今回は、可変配列を表現できるVec<u8>型を利用してバイナリデータを作成しました。

なお、今回、画像ファイルを生成する方法を学んだので、次回は画像データを扱うプログラムを作ってみましょう。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。技術書も多く執筆している。直近では、「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」「すぐに使える!業務で実践できる! PythonによるAI・機械学習・深層学習アプリのつくり方 TensorFlow2対応(ソシム)」「マンガでざっくり学ぶPython(マイナビ出版)」など。