【C言語/画像処理】PNM(PPM・PGM・PBM)ファイルの読み込みと書き出し【ライブラリ不要】

pnmの読み込み書き出し方法解説ページのアイキャッチ

このページにはプロモーションが含まれています

今回はC言語で画像処理のプログラミングしたい方向けに PNM(PPM・PGM)ファイルの読み込みと書き出し(保存)を行う方法や、その関数を紹介していきたいと思います。

私のサイトでも下記で JPEG・PNG などの読み込みと書き出し(保存)を行うプログラムの紹介をしてきました。

libjpegの使い方解説ページアイキャッチ 【C言語】libjpegのインストールと使用方法・使用例 libpngの使い方解説ページアイキャッチ 【C言語】libpngのインストールと使用方法・使用例

しかし、これらはファイル読み込みやファイル書き出しを行うために libjpeg や libpng といったライブラリを事前にインストールすることが必要でした。

そのため、インストールに手間がかかったり、設定が難しくてインストールできなかったりして画像処理プログラミングを始めるまでのハードルが若干高いです。

今回紹介する「PNM 」の場合はライブラリなど一切不要で、C言語の標準関数のみを使用するだけで画像の読み込みや保存ができるようになります。

もっと言えば、紹介しているソースコードをコピペすればプログラムから画像の読み込みや保存ができようになり、すぐに画像処理プログラミングが始められます!

このページでは、まず PNM がどんな画像ファイルであるかを説明して、続いてこれらの読み込みと保存を行うソースコードの紹介をしていきたいと思います!

PNM とは

PNM(portable any map)は下記3つの画像フォーマットの総称になります。

  • PPM:カラー画像
  • PGM:グレースケール画像
  • PBM:白黒画像(2値画像)

これら3つは扱える色が異なりますが、フォーマットとしてはほぼ同じなので、これらを総称して PNM と呼びます。

JPEG や PNG などは画像が圧縮されてファイルサイズが小さくなっていますが、PNM の場合は画像が圧縮されずファイルサイズが大きいです。

しかし、圧縮されないからこそ PNM ファイルはデータの読み込みや書き込みが簡単に行えるというメリットがあります。

PNM のデータ構造

PNM のデータの中身は下記のような構造になっています。

PNMのデータ構造

青色で囲った部分は「ヘッダー」で、緑色で囲った部分は「画像データ」です。

これら「ヘッダー」と「画像データ」の構成について説明していきます。

スポンサーリンク

ヘッダーの構成

PNM の上側のデータはヘッダーと呼ばれる部分で、その PNM の画像がどのようなものであるかの情報が格納されています。

PNMのヘッダー

このヘッダーは下記の5つの情報から構成されています。

  • マジックナンバー
  • 画像の横サイズ
  • 画像の縦サイズ
  • 最大輝度値(PBM には無い)
  • コメント

全て ASCII 形式で情報がヘッダーに格納されており、テキストエディタなどでファイルを開けばヘッダーの中身を確認することができます。

マジックナンバー

前述の通り PNM には具体的には下記の3つの画像フォーマットが存在します。

  • PPM
  • PGM
  • PBM

またこれら3つの画像フォーマットにおいて、後述で説明する画像データとして下記の2種類の形式を扱うことができます。

  • ASCII 形式
  • バイナリ形式

なので、詳細に PNM の画像フォーマットを分類すれば3種類 x 2種類 = 6種類のものが存在することになります。

マジックナンバーは、これら6種類の画像フォーマットを区別するためのものです。

このマジックナンバーは、下記のように1文字目が ‘P’ 2文字目が ‘1’ 〜 ‘6’ の数字の形式の文字列になります。

P3

各マジックナンバーが表す画像フォーマットは下記のようになります。

マジックナンバー フォーマット データ形式
P1 PBM ASCII
P2 PGM ASCII
P3 PPM ASCII
P4 PBM バイナリ
P5 PGM バイナリ
P6 PPM バイナリ

このマジックナンバーは必ず PNM ファイルの先頭に格納されています。

なので、ファイル先頭のマジックナンバーを確認することで、それ以降のデータ(特に画像データ)がどのようなデータであるかを判別することができます。

例えば PNM の画像をビューワーが表示するときは、このマジックナンバーから画像データがどのような形式かを確認して、このマジックナンバーに応じて画像データを読み込んで画面に表示する感じです。

画像の横サイズ

マジックナンバーの次に格納されているデータは「画像の横サイズ」です。

PNM画像の横サイズ

単位はピクセルになります。

ヘッダーの後に続く画像データを何ピクセル分横方向に並べれば良いかがこの情報で分かります。

画像の縦サイズ

画像の横サイズの次に格納されているデータは「画像の縦サイズ」です。

PNM画像の縦サイズ

単位はピクセルになります。

ヘッダーの後に続く画像データを何ピクセル分縦方向に並べれば良いかがこの情報で分かります。

最大輝度値

PPM と PGM の場合、画像の縦サイズの後に各ピクセルの「輝度値の最大値」が格納されています。

輝度値というのは各色の強さを表す値で、PPM の場合は「赤・緑・青」それぞれの強さを、PGM の場合は「白」の強さを表します。

この輝度値によって、そのピクセルの色が設定されます。

PBM の場合、白黒画像なのでピクセルの輝度値は必ず 1 or 0 なので最大輝度値はありません。

ピクセルや輝度値については下記で解説していますので、コチラも是非読んでみてください。

画像データの解説ページアイキャッチ 画像データの構造・画素・ビットマップデータについて解説

コメント

ヘッダーにはコメントを付けることも可能です。

コメントは ‘#’ から始まり、その行のそれ以降の部分は全てコメントとして扱われます。

# これはコメントです

各情報の区切り

PNM のヘッダーでは、各情報の間に下記のどれかを挟むことで、各情報の区切りを表すようになっています。

  • 空白
  • 行頭復帰
  • 改行
  • タブ

このページでは、これらを合わせて「空白系の文字」と呼ばせていただきます。

さらに、これの空白系の文字は2つの情報の間に「複数」挟むこともできるようです。

なので、下記のような形式でも PNM ヘッダーとして認められますし(おそらくこの形が一番基本)、

P3
256 256
255

下記や、

P3 256      256      255

下記も PNM 形式として認められます。

P3
256
256
255

PNM のファイルを読み込むプログラムを作るときには、これら様々な形式に対応できるようにプログラミングする必要があります。

画像データの構成

ヘッダーの次に格納されているデータは「画像データ」です。

PNMの画像データ

具体的にはこの「画像データ」はピクセル毎の各色の輝度値になります。

データの先頭から順に、画像左上のピクセルから右方向へのピクセルの輝度値が並んでいます。

画像データと画像座標の関係

画像の右端の次のデータは一つ下のピクセルの左端の輝度値を表しています。

こんな感じで、画像データは先頭から順に画像の左上ピクセルから右下ピクセルまでの輝度値が並べられて格納されています。

画像データと画像座標の関係2

ここまではどのマジックナンバーでも同じ話になります。

ただし、どのような形式で輝度値が格納されているかがマジックナンバーによって異なります。

ここからはマジックナンバーごとに画像データの構成について説明していきます。

説明がわかりやすいように、下記の順番で説明していきます。

  • P3
  • P2
  • P1
  • P6
  • P5
  • P4

P1 と P4 に関してはあまり使わないので説明は飛ばしても良いと思います。

P3(PPM・ASCII)

まず ASCII 形式の場合、画像データは単純に輝度値が半角英数字の「文字列」の形式で格納されていると考えて良いです。

2つの輝度値の間には空白系の文字を挟むことで、各輝度値を分別できるようにされています。

また各行は70文字以下にすることが推奨されているようです。

輝度値が6つ並べられた様子は下記のようになります。

191 161 123 201 159 132

さらに、PPM はカラー画像に用いる画像フォーマットであり、各ピクセルは赤・緑・青の3つの輝度値で色が設定されています。

なので、画像データに格納されている輝度値は3つ分で1つのピクセルの色を表すことになります。

したがって、先ほどの例で輝度値を6つ並べましたが、これで2ピクセル分の色を表していることになります。

  • 1ピクセル目:(191, 161, 123)
  • 2ピクセル目:(201, 159, 132)

P2(PGM・ASCII)

P2 の場合はほぼ P3 と考え方は同じです。

ただし P2 の場合はグレースケール画像ですので、輝度値一つが1ピクセル分の色を表すことになります。

ですので、下記の6つの輝度値で、

191 161 123 201 159 132

6ピクセル分の色が設定されていることになります。

  • 1ピクセル目:191
  • 2ピクセル目:161
  • 3ピクセル目:123
  • 4ピクセル目:201
  • 5ピクセル目:159
  • 6ピクセル目:132

P1(PBM・ASCII)

P1 の場合はほぼ P2 と考え方は同じです。

ただし P1 の場合は白黒画像ですので、輝度値は 0 or 1 しか扱えません。

0 / 1 は下記の色をそれぞれ表しています。

  • 0:白
  • 1:黒

したがって、P1 の場合の画像データは下記のようなものになります。

1 0 0 1 1 0

これで6ピクセル分の色が設定されていることになります。

  • 1ピクセル目:1(黒)
  • 2ピクセル目:0(白)
  • 3ピクセル目:0(白)
  • 4ピクセル目:1(黒)
  • 5ピクセル目:1(黒)
  • 6ピクセル目:0(白)

P6(PPM・バイナリ)

P6 の場合、P3 と同じ PPM ですがデータの形式がバイナリになります。

バイナリ形式では、画像データの各バイトのデータがそのまま輝度値となります。

また、ヘッダーの最大輝度値が 255 以下の場合、1バイトで輝度値が全て表現できますので画像データの1バイトが1つの輝度値を表すことになります。

一方、ヘッダーの最大輝度値が 256 〜 65535 の場合は2バイトで1つの輝度値を表すことになります。

バイナリ形式で輝度値が6つ並べられた様子は下記のようになります。

BFA17BC99F84

1バイトずつ(2桁ずつ)を1つの16進数として考えて10進数に変換すれば下記のような値の並びになることがわかると思います。

191 161 123 201 159 132

これはP3(PPM・ASCII)の説明で紹介したものと同じですね!

つまり、ASCII 形式とバイナリ形式では、同じ輝度値を表すのに上記のような違いがあります。

また PPM はカラー画像ですので、輝度値3つ分が1ピクセルの色を表しています。

なので、最大輝度値が 255 以下の場合は、画像データの3バイトずつが1ピクセルの色を表すことになります。

この場合、先ほどの例の輝度値6つ分が、2ピクセル分の色を表していることになります。

  • 1ピクセル目:(191, 161, 123)
  • 2ピクセル目:(201, 159, 132)

一方で最大輝度値が 256 〜 65535 の場合は、画像データの6バイトずつが1ピクセルの色を表すことになります。

P5(PGM・バイナリ)

P6 と P5 の違いはカラーかグレースケールとで扱う色が異なる点のみです。

P5 の場合はグレースケールですので、1つの輝度値が1つのピクセルの色を表すことになります。

ですので、下記の6バイトのデータは、

BFA17BC99F84

下記のように6ピクセル分の輝度を表すことになります。

  • 1ピクセル目:191
  • 2ピクセル目:161
  • 3ピクセル目:123
  • 4ピクセル目:201
  • 5ピクセル目:159
  • 6ピクセル目:132

P4(PBM・バイナリ)

P4 の場合はちょっと難易度が高いです。

P4 の場合、1ビットが1ピクセル分の輝度値を表すことになります。

一方で、基本的にプログラミングで扱うデータはバイト単位です。

ですので、1バイトの中に8つ分の輝度値を詰め込んだバイトデータを扱うことになります。

例えば下記の2バイトのデータについて考えてみましょう。

BFA1

“BF” と “A1” の16進数を2進数に変換すると下記のようになります。

1011111110100001

この2進数変換によりバイトデータの各ビットの値に分解し、各桁が1ビットデータを表すようになります。

そして、この “0” と “1” が各ピクセルの色を表すことになります。

また P4 画像においては、各バイトの最上位ビットから順に左方向のピクセルの輝度値を表しています。

MEMO

PNM を作成するアプリや PNM を表示するアプリによっては、バイトの “最下位” ビットから順に左方向からピクセルの輝度値を表す場合もあるかもしれませんので注意してください

上記の例で挙げた “BFA1” は2バイトのデータですので、下記のように16ピクセル分の色を表すことになります(バイトの最上位ビットから順に左方向のピクセルの輝度値が並んでいますので、2進数で表した時に比べて、各バイト内でのビットの並びが逆になります)。

  • 1ピクセル目:1
  • 2ピクセル目:1
  • 3ピクセル目:1
  • 4ピクセル目:1
  • 5ピクセル目:1
  • 6ピクセル目:1
  • 7ピクセル目:0
  • 8ピクセル目:1
  • 9ピクセル目:1
  • 10ピクセル目:0
  • 11ピクセル目:0
  • 12ピクセル目:0
  • 13ピクセル目:0
  • 14ピクセル目:1
  • 15ピクセル目:0
  • 16ピクセル目:1

PNM ファイルの読み込みと書き込み

それではここまで説明してきた PNM ファイルの読み込みと書き込みをソースコードを紹介して、さらにその解説をしていきたいと思います!

最大輝度値は 255 以下に限定したソースコードとなりますので、この点には注意してください。

マジックナンバー毎に解説していきますが、ここでも下記の順で解説していきたいと思います。

  • P3
  • P2
  • P1
  • P6
  • P5
  • P4

PPM と PGM とでは扱う輝度値の数が違うだけですので、一方を理解すれば他方もすぐに理解できると思います。

PBM の場合はビット演算が必要なのでちょっと難易度高いですが、使いどころが少ないため、実用性を考えればスキップしてしまっても良いと思います!

スポンサーリンク

P3 の読み込み

まずは P3 の読み込みからプログラムの紹介と説明をしていきます。

最初の解説ですので説明が長いので注意してください…。

P3 の読み込みを行う関数

P3 の PNM ファイルを読み込む関数および、さらにこの関数から実行している関数 readP3 は下記のようになります。

readP3
/**
 * P3ファイルのヘッダーと画像データをIMAGE構造体に格納する
 * 
 * 引数
 * image: 読み込んだ画像データ構造体
 * file_data: ファイルデータの先頭アドレス
 * file_size; ファイルデータのサイズ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 */
int readP3(IMAGE *image, char *file_data, unsigned int file_size) {
    unsigned int read_pos;
    unsigned int value;
    unsigned int num_byte;
    unsigned int i, j, c;
    unsigned int color;
 
    /* ヘッダーを読み込んでImage構造体にデータをつめる */
    read_pos = getImageInfo(image, file_data, file_size, PNM_TYPE_PPM_ASCII);

    if (read_pos == 0) {
        /* ヘッダー読み込みに失敗していたら終了 */
        printf("ヘッダーがおかしいです\n");
        return -1;
    }

    /* ヘッダーの情報に基づいてメモリ確保 */
    if (allocImage(image) != 0) {
        printf("メモリ取得に失敗しました\n");
        return -1;
    }

    /* 1ピクセルあたりの色の数を設定 */
    color = image->num_bit / 8;

    /* ファイル全体を読み終わるか必要な数分の輝度数をセットするまでループ */
    num_byte = 0;
    for (j = 0; j < image->height; j++) {
        for (i = 0; i < image->width; i++) {
            for (c = 0; c < color; c++) {
                unsigned int read_size;

                /* 次の値をファイルデータから取得 */
                read_size = getNextValue(&value, file_data, read_pos, file_size);

                /* 輝度値をIMAGE構造体に格納 */
                if (value > image->max_value) {

                    /* 最大輝度値を超える場合 */
                    image->data[num_byte] = (unsigned char)image->max_value;
                } else {
                    image->data[num_byte] = (unsigned char)value;
                }

                /* 格納したデータ数をインクリメント */
                num_byte += 1;

                /* データ読み込み位置と読み込んだデータ数を計算 */
                read_pos += read_size;
                
                /* ファイルサイズ分読み込んでいたら終了 */
                if (read_pos >= file_size) {
                    return 0;
                }
            }
        }
    }

    return 0;
}

使用しているIMAGE は下記のような構造体になります。

IMAGE構造体
/* 画像データ */
typedef struct {
    unsigned int width; /* 画像の横サイズ */
    unsigned int height; /* 画像の縦サイズ */
    unsigned int num_bit; /* 1ピクセルあたりのビット数 */
    unsigned int max_value; /* 最大輝度値 */
    unsigned char *data; /* 画像データの先頭アドレス */
} IMAGE;

関数の解説

readP3 は大きく下記の処理を行っています。

  1. ファイル全体のデータを読み込んでメモリにコピー
  2. 読み込んだデータのヘッダー部分の取得
    • ヘッダーの情報を IMAGE 構造体のメンバにセット
  3. ヘッダーの情報に従って画像データ用のメモリを確保
    • IMAGE 構造体の data にこのメモリを指させる
  4. 読み込んだデータから画像データ部分を取得
    • IMAGE 構造体の data の指す先のメモリにそのデータを格納

この処理の流れは P1 から P6 全てにおいて同じにしています。

大きく異なるのは最後の「読み込んだデータから画像データ部分を取得」のみです。

1. ファイル全体のデータを読み込んでメモリにコピー

まずはファイル全体を読み込んでメモリにコピーします。

これを行なっているのは readFileData 関数になります。

readFileData
/**
 * PNMファイルを読み込む
 * 
 * 引数
 * file_name: 読み込むファイルのパス
 * file_size: 読み込んだファイルのサイズ
 * 
 * 返却値
 * 成功: 読み込んだファイルデータの先頭アドレス
 * 失敗: NULL
 * 
 */
char * readFileData(char *file_name, unsigned int *file_size) {
    char *file_data = NULL;
    size_t read_size;
    unsigned int size;
    char tmp[256];
    FILE *fp;

    /* ファイルサイズ取得用にオープン */
    fp = fopen(file_name, "r");
    if (fp == NULL) {
        printf("%sが読み込めません\n", file_name);
        return NULL;
    }

    /* ファイルのサイズを取得 */
    size = 0;
    do {
        read_size = fread(tmp, 1, 256, fp);
        size += read_size;
    } while (read_size == 256);

    /* ファイルを一旦クローズ */
    fclose(fp);

    /* ファイルデータ読み込み用にオープン */
    fp = fopen(file_name, "r");
    if (fp == NULL) {
        printf("%sが読み込めません\n", file_name);
        return NULL;
    }

    /* ファイルデータ読み込み用のメモリ確保 */
    file_data = (char*)malloc(sizeof(char) * size);
    if (file_data == NULL) {
        printf("メモリが取得できません\n");
        fclose(fp);
        return NULL;
    }

    /* データの読み込み */
    read_size = fread(file_data, 1, size, fp);

    fclose(fp);

    if (read_size != size) {
        printf("読み込みサイズがおかしいです\n");
        free(file_data);
        return NULL;
    }

    /* サイズを設定 */
    *file_size = (unsigned int)size;

    /* 読み込んだデータの先頭アドレス返却 */
    return file_data;
}

ファイルを1行ずつ読み込みながらヘッダーや画像データを取得する方がメモリの節約にはなるのですが、今回はプログラムの分かりやすさを重視して一気にファイル全体を読み込んでいます。

2. 読み込んだデータのヘッダー部分の取得

次に読み込んだデータから PNM ファイルのヘッダーとなる部分を取得していきます。

これを行なっているのは getImageInfo 関数です。

getImageInfo
/**
 * ヘッダーを読み込み結果をIMAGE構造体に格納する
 * 
 * 引数
 * image: 画像データ格納用の構造体
 * file_data: ファイルデータの先頭アドレス
 * file_size: ファイルデータのサイズ
 * 
 * 返却値
 * 成功: 画像データの先頭位置
 * 失敗: 0
 */
unsigned int getImageInfo(IMAGE *image, char *file_data, unsigned int file_size, PNM_TYPE type) {

    unsigned int read_pos;
    unsigned int value;
    unsigned int read_size;

    /* データ読み込み位置を先頭にセット */
    read_pos = 0;

    /* マジックナンバー分を読み飛ばす */
    read_pos += 2;

    /* 画像の横サイズを取得する */
    read_size = getNextValue(&value, file_data, read_pos, file_size);
    image->width = value;

    read_pos += read_size;

    /* 画像の縦サイズを取得する */
    read_size = getNextValue(&value, file_data, read_pos, file_size);
    image->height = value;

    read_pos += read_size;

    /* 画像の最大輝度値を取得する */
    switch (type) {
        case PNM_TYPE_PGM_ASCII:
        case PNM_TYPE_PPM_ASCII:
        case PNM_TYPE_PGM_BINARY:
        case PNM_TYPE_PPM_BINARY:
            /* 取得するのはPGMとPBMのみ */
            read_size = getNextValue(&value, file_data, read_pos, file_size);

            /* 最大輝度値の値チェック */
            if (value > MAX_VALUE) {
                printf("最大輝度値が不正です\n");
                return 0;
            }

            image->max_value = value;
            read_pos += read_size;
            break;
        default:
            break;
    }

    /* PNMタイプに応じてピクセルあたりのバイト数を設定 */
    switch (type) {
        case PNM_TYPE_PBM_ASCII:
        case PNM_TYPE_PBM_BINARY:
            image->num_bit = 1;
            break;
        case PNM_TYPE_PGM_ASCII:
        case PNM_TYPE_PGM_BINARY:
            image->num_bit = 8;
            break;
        case PNM_TYPE_PPM_ASCII:
        case PNM_TYPE_PPM_BINARY:
            image->num_bit = 24;
            break;
        default:
            break;
    }

    return read_pos;
}

さらにこの getImageInfo 関数では getNextValue 関数を複数回実行しています。

getNextValue は下記のような関数になっています。

getNextValue
/**
 * ファイルデータの次の文字列を数値化して取得する
 * 
 * 引数
 * value: 数値化した結果
 * file: ファイルデータの先頭アドレス
 * read_pos: 読み込み位置
 * file_size: ファイルデータのサイズ
 * 
 * 返却値
 * 成功: ファイルデータから読み込んだサイズ
 * 失敗: 0
 */
unsigned int getNextValue(unsigned int *value, char *data, unsigned int read_pos, unsigned int file_size) {
    char str[256];

    /* 空白系の文字やコメントを除いた次の文字列を取得する */
    unsigned int i, j, k;
    
    i = 0;
    while (read_pos + i < file_size) {
        /* 空白系の文字の場合は次の文字へスキップ */
        if (isspace(data[read_pos + i])) {
            i++;
            continue;
        }

        /* #ならそれ以降はコメントなので次の行へ */
        if (data[read_pos + i] == '#') {
            do {
                i++;
            } while (read_pos + i < file_size && data[read_pos + i] != '\n');

            /* \nの1文字文進める */
            i++;
        }

        break;
    }

    /* 文字列を取得 */
    j = 0;
    while (read_pos + i + j < file_size && !isspace(data[read_pos + i + j])) {
        /* 読み込んだバイト数をカウントアップ */
        j++;
    }

    /* 文字列を数字に変換 */
    for (k = 0; k < j; k++) {
        str[k] = data[read_pos + i + k];
    }
    str[k] = '\0';

    /* int化 */
    *value = (unsigned int)atoi(str);

    /* 読み込んだ文字数を返却 */
    return (i + j);
}

getNextValue は引数の file_data の引数 read_pos の位置からデータを読み込み、次に見つかった数値を引数 value のアドレスに格納する関数です。

各情報の区切りで解説したように、ヘッダーの各情報は空白系の文字で区切られています。複数個の空白系の文字で区切られることもあります。

さらにヘッダーにはコメントをつけることも可能で “#” 以降は行の最後までコメントとして扱われます。

これらは画像の情報としては意味がないものですので、下記によりこれらを読み飛ばすようにしています。

空白系文字とコメントの読み飛ばし
/* 空白系の文字の場合は次の文字へスキップ */
if (isspace(data[read_pos + i])) {
    i++;
    continue;
}

/* #ならそれ以降はコメントなので次の行へ */
if (data[read_pos + i] == '#') {
    do {
        i++;
    } while (read_pos + i < file_size && data[read_pos + i] != '\n');

     /* \nの1文字文進める */
     i++;
}

isspace 関数はまさに引数で指定した文字が「空白系の文字」かどうかを判断する関数です。

そして、空白系の文字もしくはコメント以外の文字が見つかった時に、その文字から次の空白系の文字までを読み込み、それを atoi 関数で数値に変換する処理を行なっています。

さらに、getNextValue 関数は「関数内で読み飛ばした文字数と読み込んだ文字数」を返却するようにしています。

ですので getNextValue 関数の呼び出し側で下記のように read_pos をその返却値分増やしてから再度 getNextValue 関数を呼び出すことで、ファイルのデータの先頭方向から順々に値のみを取得する事ができます。

getNextValueでデータを取得する様子

getImageInfo 関数では、この getNextValue を使いながら下記のヘッダーの情報を取得し、その情報を IMAGE 構造体の各メンバにセットする処理を行なっています。

  • 画像の横サイズ
  • 画像の縦サイズ
  • 最大輝度値(PBM には無い)

他にも PNM のタイプに応じて下記の情報もセットしています。

  • num_bit:1ピクセルあたりのビット数

3. ヘッダーの情報に従って画像データ用のメモリを確保

次に行うのが画像データ格納用のメモリの確保を allocImage 関数で行なっています。

allocImage
/**
 * 画像データ格納用のバッファを確保する
 * 
 * 引数
 * image: 画像データ格納用の構造体
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 */
int allocImage(IMAGE *image) {
    unsigned int size;
    unsigned char *data;
    unsigned int line_byte;

    if (image == NULL) {
        return -1;
    }

    /* 1行あたりのバイト数を計算(切り上げ) */
    line_byte = (image->width * image->num_bit + 7) / 8;

    /* サイズを決定してメモリ取得 */
    size = line_byte * image->height;
    data = (unsigned char *)malloc(sizeof(unsigned char) * size);
    if (data == NULL) {
        printf("mallocに失敗しました\n");
        return -1;
    }

    /* 取得したメモリのアドレスをimage構造体にセット */
    image->data = data;

    return 0;

}

この関数で行なっているのは下記の二つです。

  • 必要なメモリサイズの計算
  • malloc 関数でのメモリの確保

必要なメモリサイズは下記の式で求まります。

画像の横サイズ x 画像の縦サイズ x 1ピクセルあたりのビット数 / 8 [Byte]

8 で割った時に余りが発生した場合は切り上げを行うように計算しています。
MEMO

PPM や PGM の場合は余りが発生することはありません

PBM のみ余りが発生します

最大輝度値が 255 以下の場合は各輝度値が1バイトで表現できるので上の式で良いですが、最大輝度値が 256 以上の場合は各輝度値を表現するのに2バイト必要ですので、「上式で求まるサイズ × 2」分のメモリが必要になります。

4. 読み込んだデータから画像データ部分を取得

ここまでで画像データを取得して IMAGE 構造体に格納する準備は整ったことになります。

次はいよいよ画像データの取得を行います。

readP3allocImage 実行の後ろ側がこの処理になります。

P3 の場合は ASCII 形式で輝度値がファイルデータに格納されており、各輝度値が空白系の文字で区切られています。

ですので、先程紹介した getNextValue を使用して輝度値を取得し、

輝度値の取得
/* 次の値をファイルデータから取得 */
read_size = getNextValue(&value, file_data, read_pos, file_size);

取得した値を IMAGE 構造体(の data の指すメモリ)に格納する処理を繰り返し行うことで、画像データの取得を実現することができます。

輝度値の格納
image->data[num_byte] = (unsigned char)value;

最大輝度値が 255 以下の場合、各輝度値全て1バイトで表現できるので、1つの輝度値を格納した際に1バイト分アドレスを増加させ、増加させたアドレスに対して次の輝度値を格納するようにしています。

格納位置の移動
/* 格納したデータ数をインクリメント */
num_byte += 1;

全ピクセル分の輝度値を取得した or ファイルデータの最後までデータを読み込んだ時点で画像データの取得は終了させるようにしています。

もうちょっと細かい処理もしていますが、基本的な流れは上記の処理となります。

ここから他のフォーマットの読み込みについて解説していきますが、「4. 読み込んだデータから画像データ部分を取得」以外は同じ処理になるので説明は省略させていただきます。

P2 の読み込み

次は P2 の読み込みのプログラムの紹介と説明をしていきます。

P2 の読み込みを行う関数

P2 の PNM ファイルを読み込む関数 readP2 は下記のようになります。

readP2
/**
 * P2ファイルのヘッダーと画像データをIMAGE構造体に格納する
 * 
 * 引数
 * image: 読み込んだ画像データ構造体
 * file_data: ファイルデータの先頭アドレス
 * file_size; ファイルデータのサイズ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 */
int readP2(IMAGE *image, char *file_data, unsigned int file_size) {
    unsigned int read_pos;
    unsigned int value;
    unsigned int num_byte;
    unsigned int i, j, c;
    unsigned int color;
 
    /* ヘッダーを読み込んでImage構造体にデータをつめる */
    read_pos = getImageInfo(image, file_data, file_size, PNM_TYPE_PGM_ASCII);

    if (read_pos == 0) {
        /* ヘッダー読み込みに失敗していたら終了 */
        printf("ヘッダーがおかしいです\n");
        return -1;
    }

    /* ヘッダーの情報に基づいてメモリ確保 */
    if (allocImage(image) != 0) {
        printf("メモリ取得に失敗しました\n");
        return -1;
    }

    /* 1ピクセルあたりの色の数を設定 */
    color = image->num_bit / 8;

    /* ファイル全体を読み終わるか必要な数分の輝度数をセットするまでループ */
    num_byte = 0;
    for (j = 0; j < image->height; j++) {
        for (i = 0; i < image->width; i++) {
            for (c = 0; c < color; c++) {
                unsigned int read_size;

                /* 次の値をファイルデータから取得 */
                read_size = getNextValue(&value, file_data, read_pos, file_size);

                /* 輝度値をIMAGE構造体に格納 */
                if (value > image->max_value) {

                    /* 最大輝度値を超える場合 */
                    image->data[num_byte] = (unsigned char)image->max_value;
                } else {
                    image->data[num_byte] = (unsigned char)value;
                }

                /* 格納したデータ数をインクリメント */
                num_byte += 1;

                /* データ読み込み位置と読み込んだデータ数を計算 */
                read_pos += read_size;
                
                /* ファイルサイズ分読み込んでいたら終了 */
                if (read_pos >= file_size) {
                    return 0;
                }
            }
        }
    }

    return 0;
}

関数の解説

readP2readP3 とほぼ同じ関数になります。

ただし、getImageInfo 関数でセットされる IMAGE 構造体の num_bit8 になりますので、1ピクセルあたりの輝度値が1つのみになり、その分読み込む画像データのサイズが小さくなります。

違いはこれだけです。

P1 の読み込み

次は P1 の読み込みのプログラムの紹介と説明をしていきます。

P1 の読み込みを行う関数

P1 の PNM ファイルを読み込む関数 readP1 は下記のようになります。

readP1
/**
 * P1ファイルのヘッダーと画像データをIMAGE構造体に格納する
 * 
 * 引数
 * image: 読み込んだ画像データ構造体
 * file_data: ファイルデータの先頭アドレス
 * file_size; ファイルデータのサイズ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 */
int readP1(IMAGE *image, char *file_data, unsigned int file_size) {
    unsigned int read_pos;
    unsigned int value;
    unsigned int num_byte;
    unsigned char byte;
    unsigned int num_bit;
    unsigned int i, j;
 
    /* ヘッダーを読み込んでImage構造体にデータをつめる */
    read_pos = getImageInfo(image, file_data, file_size, PNM_TYPE_PBM_ASCII);

    if (read_pos == 0) {
        /* ヘッダー読み込みに失敗していたら終了 */
        printf("ヘッダーがおかしいです\n");
        return -1;
    }

    /* ヘッダーの情報に基づいてメモリ確保 */
    if (allocImage(image) != 0) {
        printf("メモリ取得に失敗しました\n");
        return -1;
    }

    /* ファイル全体を読み終わるか必要な数分の輝度数をセットするまでループ */
    num_byte = 0;
    num_bit = 0;
    byte = 0;
    for (j = 0; j < image->height; j++) {
        for (i = 0; i < image->width; i++) {

            unsigned int read_size;

            /* 次の値をファイルデータから取得 */
            read_size = getNextValue(&value, file_data, read_pos, file_size);

            /* ビットをバイトに詰める */
            byte += value << num_bit;
            num_bit++;
            if (num_bit >= 8) {
                /* 1バイト分詰め込んだらimage構造体に格納 */
                image->data[num_byte] = byte;
                byte = 0;

                /* 次のバイトにビットを詰め込んでいく */
                num_bit = 0;
                num_byte++;
            }


            /* データ読み込み位置と読み込んだデータ数を計算 */
            read_pos += read_size;
                
            /* ファイルサイズ分読み込んでいたら終了 */
            if (read_pos >= file_size) {
                return 0;
            }
        }
        if (num_bit != 0) {
            /* 次の行は最下位ビットからバイトに詰めていく */
            image->data[num_byte] = byte;
            byte = 0;
            num_bit = 0;
            num_byte++;
        }
    }

    return 0;
}

関数の解説

P1 では読み込む輝度値が “0” or “1” の2種類なので、各輝度値を1ビットデータで扱う事ができます。

なので、読み込んだ値を1バイトに8つ分詰め込んでから IMAGE 構造体の data に格納するようにしています。

それを行なっているのが下記になります。バイトデータの下位ビットから順に輝度値を順々にセットし、

取得した値をバイトに詰める様子
/* 次の値をファイルデータから取得 */
read_size = getNextValue(&value, file_data, read_pos, file_size);

/* ビットをバイトに詰める */
byte += value << num_bit;
num_bit++;

さらに8ビット分輝度値のセットが完了したら data に値を格納します。

dataに格納する様子
if (num_bit >= 8) {
    /* 1バイト分詰め込んだらimage構造体に格納 */
    image->data[num_byte] = byte;
    byte = 0;

    /* 次のバイトにビットを詰め込んでいく */
    num_bit = 0;
    num_byte++;
}

これで1バイトに8ピクセル分の輝度値が格納されることになります。

1バイト分のデータを格納すれば、次はまたバイトの最下位ビットから順に輝度値をセットしていきます。

スポンサーリンク

P6 の読み込み

次は P6 の読み込みのプログラムの紹介と説明をしていきます。

P6 の読み込みを行う関数

P6 の PNM ファイルを読み込む関数 readP6 は下記のようになります。

readP6
/**
 * P6ファイルのヘッダーと画像データをIMAGE構造体に格納する
 * 
 * 引数
 * image: 読み込んだ画像データ構造体
 * file_data: ファイルデータの先頭アドレス
 * file_size; ファイルデータのサイズ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 * 
 */
int readP6(IMAGE *image, char *file_data, unsigned int file_size) {
    unsigned int read_pos;
    unsigned int num_byte;
    unsigned int i, j, c;
    unsigned char byte_data;
    unsigned int color;
 
    /* ヘッダーを読み込んでImage構造体にデータをつめる */
    read_pos = getImageInfo(image, file_data, file_size, PNM_TYPE_PPM_BINARY);

    if (read_pos == 0) {
        /* ヘッダー読み込みに失敗していたら終了 */
        printf("ヘッダーがおかしいです\n");
        return -1;
    }

    /* ヘッダーの情報に基づいてメモリ確保 */
    if (allocImage(image) != 0) {
        printf("メモリ取得に失敗しました\n");
        return -1;
    }

    /* 最大輝度値の次にある空白系文字の分、読み込み位置を加算 */
    read_pos += 1;

    /* 1ピクセルあたりの色の数を設定 */
    color = image->num_bit / 8;

    /* ファイル全体を読み終わるか必要な数分の輝度数をセットするまでループ */
    num_byte = 0;
    for (j = 0; j < image->height; j++) {
        for (i = 0; i < image->width; i++) {
            for (c = 0; c < color; c++) {

                /* 輝度値をIMAGE構造体に格納 */
                byte_data = (unsigned char)file_data[read_pos];
                image->data[num_byte] = byte_data;

                /* 格納したデータ数をインクリメント */
                num_byte += 1;

                /* データ読み込み位置と読み込んだデータ数を計算 */
                read_pos += 1;
                
                /* ファイルサイズ分読み込んでいたら終了 */
                if (read_pos >= file_size) {
                    return 0;
                }
            }
        }
    }

    return 0;
}

関数の解説

P3 に対し、P6 では画像データの形式がバイナリになります。

最大輝度値が 255 以下の場合は1バイトに1つの輝度値が格納されていることになります。

また file_data 配列の方は char にしていますので、配列の1つの要素が1バイトのデータとなります(char のサイズが1バイトなので)。

ですので、ループの中で file_data から順々に要素を取得しやり、それを image->data に格納してやることで、P6 の画像データを取得することができることになります。

1バイトデータの取得
/* 輝度値をIMAGE構造体に格納 */
byte_data = (unsigned char)file_data[read_pos];
image->data[num_byte] = byte_data;

P5 の読み込み

次は P5 の読み込みのプログラムの紹介と説明をしていきます。

P5 の読み込みを行う関数

P5 の PNM ファイルを読み込む関数 readP5 は下記のようになります。

readP5
/**
 * P5ファイルのヘッダーと画像データをIMAGE構造体に格納する
 * 
 * 引数
 * image: 読み込んだ画像データ構造体
 * file_data: ファイルデータの先頭アドレス
 * file_size; ファイルデータのサイズ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 * 
 */
int readP5(IMAGE *image, char *file_data, unsigned int file_size) {
    unsigned int read_pos;
    unsigned int num_byte;
    unsigned int i, j, c;
    unsigned char byte_data;
    unsigned int color;
 
    /* ヘッダーを読み込んでImage構造体にデータをつめる */
    read_pos = getImageInfo(image, file_data, file_size, PNM_TYPE_PPM_BINARY);

    if (read_pos == 0) {
        /* ヘッダー読み込みに失敗していたら終了 */
        printf("ヘッダーがおかしいです\n");
        return -1;
    }

    /* ヘッダーの情報に基づいてメモリ確保 */
    if (allocImage(image) != 0) {
        printf("メモリ取得に失敗しました\n");
        return -1;
    }

    /* 最大輝度値の次にある空白系文字の分、読み込み位置を加算 */
    read_pos += 1;

    /* 1ピクセルあたりの色の数を設定 */
    color = image->num_bit / 8;

    /* ファイル全体を読み終わるか必要な数分の輝度数をセットするまでループ */
    num_byte = 0;
    for (j = 0; j < image->height; j++) {
        for (i = 0; i < image->width; i++) {
            for (c = 0; c < color; c++) {

                /* 輝度値をIMAGE構造体に格納 */
                byte_data = (unsigned char)file_data[read_pos];
                image->data[num_byte] = byte_data;

                /* 格納したデータ数をインクリメント */
                num_byte += 1;

                /* データ読み込み位置と読み込んだデータ数を計算 */
                read_pos += 1;
                
                /* ファイルサイズ分読み込んでいたら終了 */
                if (read_pos >= file_size) {
                    return 0;
                }
            }
        }
    }

    return 0;
}

関数の解説

readP5 は readP6 と同じ関数です。が、P6 に比べて色の数が少ないので、その分ループ回数も少なくなります。

P4 の読み込み

読み込みの最後として、P4 の読み込みのプログラムの紹介と説明をしていきます。

P4 の読み込みを行う関数

P4 の PNM ファイルを読み込む関数 readP4 は下記のようになります。

readP4
/**
 * P4ファイルのヘッダーと画像データをIMAGE構造体に格納する
 * 
 * 引数
 * image: 読み込んだ画像データ構造体
 * file_data: ファイルデータの先頭アドレス
 * file_size; ファイルデータのサイズ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 * 
 */
int readP4(IMAGE *image, char *file_data, unsigned int file_size) {
    unsigned int read_pos;
    unsigned char byte_data;
    unsigned int num_byte;
    unsigned char byte;
    unsigned int num_bit;
    unsigned int i, j;
 
    /* ヘッダーを読み込んでImage構造体にデータをつめる */
    read_pos = getImageInfo(image, file_data, file_size, PNM_TYPE_PBM_BINARY);

    if (read_pos == 0) {
        /* ヘッダー読み込みに失敗していたら終了 */
        printf("ヘッダーがおかしいです\n");
        return -1;
    }

    /* ヘッダーの情報に基づいてメモリ確保 */
    if (allocImage(image) != 0) {
        printf("メモリ取得に失敗しました\n");
        return -1;
    }

    /* 画像の縦サイズの次にある空白系文字の分、読み込み位置を加算 */
    read_pos += 1;

    /* ファイル全体を読み終わるか必要な数分の輝度数をセットするまでループ */
    num_byte = 0;
    
    /* ファイルサイズ分読み込んでいたら終了 */
    if (read_pos >= file_size) {
        return 0;
    }

    /* ファイル全体を読み終わるか必要な数分の輝度数をセットするまでループ */
    num_byte = 0;
    for (j = 0; j < image->height; j++) {
        for (i = 0; i < (image->width + 7) / 8; i++) {

            /* 輝度値をIMAGE構造体に格納 */
            byte_data = bitSwap((unsigned char)file_data[read_pos]);
            image->data[num_byte] = byte_data;

            /* 格納したデータ数をインクリメント */
            num_byte += 1;

            /* データ読み込み位置と読み込んだデータ数を計算 */
            read_pos += 1;
            
            /* ファイルサイズ分読み込んでいたら終了 */
            if (read_pos >= file_size) {
                return 0;
            }
        }
    }

    return 0;
}

関数の解説

readP4 も基本的な処理の流れは readP6 と同じです。

1つの輝度値は1ビットになりますが、ファイルとして既に8つの輝度値が1バイトに詰められた状態になっていますので、1バイトずつ読み込んで1バイトずつ image->data に格納してやることで P4 の画像データを読み込むことができます。

ただし、P4(PBM・バイナリ)で解説したように、バイトデータの中は上位ビットから順に左側のピクセルの輝度値が格納されているので、それを下位ビットから順に左側のピクセルの輝度値が格納されるように、ビットの並び順を逆順にする bitSwap 関数を実行するようにしています。

これは下位ビットから順に左側のピクセルの輝度値が格納されている方が、画像処理を行う時にデータを扱いやすいからです。

ビットの並び順の変更
/* 輝度値をIMAGE構造体に格納 */
byte_data = bitSwap((unsigned char)file_data[read_pos]);
image->data[num_byte] = byte_data;

スポンサーリンク

P3 の書き込み

ここからは書き込みを行うプログラムの紹介と説明を行っていきます。まずは一番基本的な P3 になります。

P3 の書き込みを行う関数

P3 の PNM ファイルを書き込みを行う関数 writeP3 は下記のようになります。

writeP3
/**
 * P3ファイルをファイル保存
 * 
 * 引数
 * image: 保存する画像の情報
 * fp: 保存先ファイルへのファイルポインタ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 */
int writeP3(IMAGE *image, FILE *fp) {
    
    unsigned int num_data;
    unsigned int line_byte;

    /* ヘッダーに情報書き出し */
    fprintf(fp, "P3\n");
    fprintf(fp, "%u %u\n", image->width, image->height);
    fprintf(fp, "%u\n", image->max_value);

    line_byte = (image->width * image->num_bit + 7) / 8;
    num_data = 0;

    /* 全輝度値をファイルに書き出し */
    while (num_data < line_byte * image->height) {
        /* ASCII形式でファイルに書き出し */
        fprintf(fp, "%u ", image->data[num_data]);

        /* 適度に改行を入れておく */
        if (num_data % 20 == 19) {
            fprintf(fp, "\n");
        }

        /* 書き出した輝度値の数をインクリメント */
        num_data += 1;
    }

    return 0;
}

関数の解説

書き込みを行う関数の基本的流れは下記のようになります。

  • ヘッダーの情報をファイルに書き出し
  • 画像データをファイルに書き出し

ヘッダーの情報はIMAGE 構造体に格納されていますので、その情報をまず ASCII 形式でファイルに書き出しします。

ASCII 形式でファイルに書き出しを行う場合には fprintf 関数を使用すれば楽に書き出しが行えます。

画像データは IMAGE 構造体の data の指すメモリに格納されていますので、この data からデータを取得し、それをヘッダー同様に ASCII 形式で書き出ししています。

データのASCII形式での書き出し
/* ASCII形式でファイルに書き出し */
fprintf(fp, "%u ", image->data[num_data]);

これを画像データのサイズ分繰り返し実行してやることで、画像データの書き出しを行うことができます。

読み込みよりもシンプルに処理を行えると思います。

P2 の書き込み

次は P2 の書き込みを行うプログラムの紹介と説明を行っていきます。

P2 の書き込みを行う関数

P2 の PNM ファイルを書き込みを行う関数 writeP2 は下記のようになります。

writeP2
/**
 * P2ファイルをファイル保存
 * 
 * 引数
 * image: 保存する画像の情報
 * fp: 保存先ファイルへのファイルポインタ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 */
int writeP2(IMAGE *image, FILE *fp) {
    
    unsigned int num_data;
    unsigned int line_byte;

    /* ヘッダーに情報書き出し */
    fprintf(fp, "P2\n");
    fprintf(fp, "%u %u\n", image->width, image->height);
    fprintf(fp, "%u\n", image->max_value);

    line_byte = (image->width * image->num_bit + 7) / 8;
    num_data = 0;

    /* 全輝度値をファイルに書き出し */
    while (num_data < line_byte * image->height) {
        /* ASCII形式でファイルに書き出し */
        fprintf(fp, "%u ", image->data[num_data]);

        /* 適度に改行を入れておく */
        if (num_data % 20 == 19) {
            fprintf(fp, "\n");
        }

        /* 書き出した輝度値の数をインクリメント */
        num_data += 1;
    }

    return 0;
}

関数の解説

writeP2writeP3 とほぼ同じ関数になります。

image->num_bit が P3 の時よりも小さくなるので、その分画像データ書き出し時のループ回数は少ないです。

またヘッダーに書き出しするマジックナンバーが P2 になることにも注意が必要です。

P1 の書き込み

次は P1 の書き込みを行うプログラムの紹介と説明を行っていきます。

P1 の書き込みを行う関数

P1 の PNM ファイルを書き込みを行う関数 writeP1 は下記のようになります。

writeP1
/**
 * P1ファイルをファイル保存
 * 
 * 引数
 * image: 保存する画像の情報
 * fp: 保存先ファイルへのファイルポインタ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 */
int writeP1(IMAGE *image, FILE *fp) {

    unsigned int num_bit;
    unsigned int num_byte;
    unsigned int num_output;
    unsigned char bit_data;
    unsigned char byte_data;
    unsigned int i, j;

    /* ヘッダーに情報書き出し */
    fprintf(fp, "P1\n");
    fprintf(fp, "%u %u\n", image->width, image->height);

    num_byte = 0;
    num_bit = 0;
    num_output = 0;

    /* 全輝度値をファイルに書き出し */
    for (j = 0; j < image->height; j++) {
        for (i = 0; i < image->width; i++) {
            byte_data = image->data[num_byte];
            bit_data = (byte_data & (1 << num_bit)) >> num_bit;

            /* ASCII形式でファイルに書き出し */
            fprintf(fp, "%u ", bit_data);
            num_output++;
            num_bit++;

            /* 適度に改行を入れておく */
            if (num_output % 60 == 59) {
                fprintf(fp, "\n");
            }

            if (num_bit >= 8) {
                num_byte++;
                num_bit = 0;
            }
        }
        if (num_bit != 0) {
            num_bit = 0;
            num_byte++;
        }
    }

    return 0;
}

関数の解説

1つずつ輝度値を書き出していくところは P3 等と同様ですが、P1 の場合は1バイトの中に8つの輝度値が格納されていることになりますので、そのバイトの中から1ビットのデータに分解し、分解後のデータをファイル書き出しする必要があります。

この辺りの処理を行っているのが下記になります。

ビットへの分解
byte_data = image->data[num_byte];
bit_data = (byte_data & (1 << num_bit)) >> num_bit;

/* ASCII形式でファイルに書き出し */
fprintf(fp, "%u ", bit_data);

スポンサーリンク

P6 の書き込み

次は P6 の書き込みを行うプログラムの紹介と説明を行っていきます。

ここからはバイナリ形式でファイル書き出しを行っていく必要があります。

P6 の書き込みを行う関数

P6 の PNM ファイルを書き込みを行う関数 writeP6 は下記のようになります。

writeP6
/**
 * P6ファイルをファイル保存
 * 
 * 引数
 * image: 保存する画像の情報
 * fp: 保存先ファイルへのファイルポインタ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 */
int writeP6(IMAGE *image, FILE *fp) {
    
    unsigned int num_data;
    unsigned int line_byte;

    /* ヘッダーに情報書き出し */
    fprintf(fp, "P6\n");
    fprintf(fp, "%u %u\n", image->width, image->height);
    fprintf(fp, "%u\n", image->max_value);

    line_byte = (image->width * image->num_bit + 7) / 8;
    num_data = 0;

    /* 全輝度値をファイルに書き出し */
    while (num_data < line_byte * image->height) {

        /* バイナリ形式でファイルに書き出し */
        fputc(image->data[num_data], fp);

        /* 書き出した輝度値の数をインクリメント */
        num_data += 1;
    }

    return 0;
}

関数の解説

fputc 関数を使えば、引数で指定した値をバイナリ形式でファイルに書き出しすることができます。

ですので、バイナリ形式の場合は image->data から取得した値をそのまま fputc 関数でファイル書き出ししてやれば良いだけです。

P5 の書き込み

次は P5 の書き込みを行うプログラムの紹介と説明を行っていきます。

P5 の書き込みを行う関数

P5 の PNM ファイルを書き込みを行う関数 writeP5 は下記のようになります。

writeP5
/**
 * P5ファイルをファイル保存
 * 
 * 引数
 * image: 保存する画像の情報
 * fp: 保存先ファイルへのファイルポインタ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 */
int writeP6(IMAGE *image, FILE *fp) {
    
    unsigned int num_data;
    unsigned int line_byte;

    /* ヘッダーに情報書き出し */
    fprintf(fp, "P5\n");
    fprintf(fp, "%u %u\n", image->width, image->height);
    fprintf(fp, "%u\n", image->max_value);

    line_byte = (image->width * image->num_bit + 7) / 8;
    num_data = 0;

    /* 全輝度値をファイルに書き出し */
    while (num_data < line_byte * image->height) {

        /* バイナリ形式でファイルに書き出し */
        fputc(image->data[num_data], fp);

        /* 書き出した輝度値の数をインクリメント */
        num_data += 1;
    }

    return 0;
}

関数の解説

ご覧の通りほぼ P6 と同じ処理でファイル書き込みすることができます。

P4 の書き込み

最後に P4 の書き込みを行うプログラムの紹介と説明を行っていきます。

P4 の書き込みを行う関数

P4 の PNM ファイルを書き込みを行う関数 writeP4 は下記のようになります。

writeP4
/**
 * P4ファイルをファイル保存
 * 
 * 引数
 * image: 保存する画像の情報
 * fp: 保存先ファイルへのファイルポインタ
 * 
 * 返却値
 * 成功: 0
 * 失敗: 0以外
 */
int writeP4(IMAGE *image, FILE *fp) {
    
    unsigned int num_data;
    unsigned int line_byte;

    /* ヘッダーに情報書き出し */
    fprintf(fp, "P4\n");
    fprintf(fp, "%u %u\n", image->width, image->height);

    line_byte = (image->width * image->num_bit + 7) / 8;
    num_data = 0;

    /* 全輝度値をファイルに書き出し */
    while (num_data < line_byte * image->height) {

        /* バイナリ形式でファイルに書き出し */
        fputc(bitSwap(image->data[num_data]), fp);

        /* 書き出した輝度値の数をインクリメント */
        num_data += 1;
    }
    
    return 0;
}

関数の解説

writeP4 も基本的な処理の流れは writeP6 と同じです。

ただし、P4(PBM・バイナリ)で解説したように、PNM ファイルの画像データでは、バイトデータの中は上位ビットから順に左側のピクセルの輝度値が格納されている必要があります。

一方で、このページで紹介している IMAGE 構造体の data に格納する画像データはバイトデータの中の下位ビットから順に左側のピクセルの輝度値を格納するようにしているので、ファイル書き出しする時にビットの並び順を逆順にする bitSwap 関数を実行するようにしています。

ビットの並び順の変更
/* バイナリ形式でファイルに書き出し */
fputc(bitSwap(image->data[num_data]), fp);

スポンサーリンク

まとめ

このページでは、まず PNM について解説し、各マジックナンバーの形式(P1 から P6)のデータ構造やそれぞれのファイルからの読み込み・ファイルへの書き込み方法について解説しました。

JPEG や PNG だと画像が圧縮されていますが、PNM の場合は画像が圧縮されていないのでデータの読み込みはほぼファイルからデータを読み込めば良いだけになっています。

ですのでライブラリ不要で画像データを自身のプログラムで簡単に扱えるようになります!

ライブラリインストールすることなく気軽に画像処理プログラミングを始めたい方におすすめの画像フォーマットになります。

是非このページで紹介した方法や関数を利用して、画像処理プログラミングを始めてみてください!

このページで紹介した関数を実際に利用しているプログラムは下記ページで紹介しています。

具体的な使い方など分かると思いますので、是非こちらも合わせて読んでみてください。

PNMフォーマット相互変換のプログラム紹介ページアイキャッチ 【C言語/画像処理】PPM・PGM・PBMの相互変換プログラム【ライブラリ不要】

同じカテゴリのページ一覧を表示