画像データの取り扱い

作成:

前回は、1ピクセルの情報を記述する方法について検討した。 今回は、そのピクセルの集合体である画像を表現する方法を検討する。

今回は、image.h及び image.cの説明となる。

画像データの表現

ひとつのピクセルの表現方法が決まったところで、画像の表現方法を考える。

画像は、ピクセルを2次元に並べたものなので、 必要最小限の情報は縦横の大きさと、それを埋め尽くす数のピクセル要素があれば良い。 ピクセル情報の格納方法は、全体を格納する配列に格納してもよいだろうし、 実際に配置する方法に合わせて2次元の配列にしても良いだろう。

インデックスカラーも表現できるようにする必要があるので、 カラーパレットとそのパレットの数も必要になる。 また、1ピクセルの色の表現方法が複数あるため、 そのどの方式なのかを表現するパラメータも必要だろう。

以上を加味して、画像を以下の構造体で表現することにする。

#define COLOR_TYPE_INDEX 0  /**< インデックスカラー方式 */
#define COLOR_TYPE_GRAY  1  /**< グレースケール方式 */
#define COLOR_TYPE_RGB   2  /**< RGB方式 */
#define COLOR_TYPE_RGBA  3  /**< RGBA方式 */

typedef struct image_t {
  uint32_t width;       /**< 幅 */
  uint32_t height;      /**< 高さ */
  uint16_t color_type;  /**< 色表現の種別 */
  uint16_t palette_num; /**< カラーパレットの数 */
  color_t *palette;     /**< カラーパレットへのポインタ */
  pixcel_t **map;       /**< 画像データ */
} image_t;

ほぼ、見てもらえば分かると思うが、順に説明していく。

まずは、widthheight、画像の幅と高さの情報を 32bit 符号なし整数で表現することにしている。 普通に int で十分なのだが、ビット幅を明確にするとともに、 負の値になることはないのでuint32_tとしている。

次に、color_typeとして、色の表現方法を表すパラメータを用意している。 前回の終わりに説明したが、各ピクセルの情報を表現する共用体単体ではどの表現方法が使われているかわからないため、 画像の情報としてパラメータを用意し、その取りうる値を#defineで定義している。 また、アルファ値を見る必要があるのかどうかを簡単に区別できるように、 RGB と RGBA の定義を分けるようにした。 今のところ 4 パターンで、今後増えたとしても 256 パターンを超えることはないだろうが、 次のカラーパレットの数のパラメータと合わせてきりの良い 4byte になるように 2byte を割り当て、 uint16_tとしている。

次に、paletteカラーパレットの情報で、インデックスカラー方式の時のみ参照されるパラメータである。 カラーパレットは色情報の配列として表現するが、インデックスカラー以外では不要なので、 ポインタにして別途メモリ確保を行うようにした。 パレットとしていくつ持っているのかの情報も必要になるため、palette_numを用意している。 パレットの数は最大で256個、パレットなしを表す0も表現しないといけないので実は8bitでは足りない。 そのためuint16_tとしている。

最後に、map、肝心の画像データ部分だ。 一つの配列でも良いのだが、今後の様々な取り回しを考えると2次元配列で表現しておいたほうがよいと判断し、 ポインタのポインタを配置している。行方向の配列を保持する列配列という形だ。

画素の格納順序

上図のように、行配列の格納順は上から、 行内のピクセルの格納順は左からと、横書きの文章と同じ順序になるようにしている。 画像上の座標では負の座標は普通は扱わないため、いわゆる第一象限のみの座標系になる。 数学で言う第一象限の座標系は左下を原点として、x軸は右向き、y軸は上向きを正とした座標系を持っている。 しかし、ディスプレイやプリンタなど、多くの画像を扱うハードウェアは左上から処理をするため、 多くの画像フォーマットでこのような原点を左上として、y軸が下向きの座標系が用いられる。 このことはプログラミングの世界では一般的だが、初めて触れるユーザにとっては少し違和感を覚える部分ではないかと思う。

画像データ部分やカラーパレット部分は別途メモリ確保を行って使用する前提としているので、 メモリの確保、開放、また、画像データのコピーを行う関数が必要だろう。 そこで以下の様な関数を用意しておく。

メモリ確保関数

各処理で必要に応じて内部のメモリを確保していっても良いのだが、 ほとんどの処理では単にこの大きさの画像情報が格納できる構造体が欲しい、ということになるので サイズと形式を指定して、内部のメモリと合わせてメモリ確保・初期化を行う関数を用意する。

image_t *allocate_image(uint32_t width, uint32_t height, uint8_t type) {
  uint32_t i;
  image_t *img;
  if ((img = calloc(1, sizeof(image_t))) == NULL) {
    return NULL;
  }
  img->width = width;
  img->height = height;
  img->color_type = type;
  if (type == COLOR_TYPE_INDEX) {
    if ((img->palette = calloc(256, sizeof(color_t))) == NULL) {
      goto error;
    }
  } else {
    img->palette = NULL;
  }
  img->palette_num = 0;
  if ((img->map = calloc(height, sizeof(pixcel_t*))) == NULL) {
    goto error;
  }
  for (i = 0; i < height; i++) {
    if ((img->map[i] = calloc(width, sizeof(pixcel_t))) == NULL) {
      goto error;
    }
  }
  return img;
  error:
  free_image(img);
  return NULL;
}

メモリ開放関数

内部で確保したメモリを個別に開放していくのは煩雑なので、 やはり、それら処理をまとめて行う関数を用意する。

void free_image(image_t *img) {
  uint32_t i;
  if (img == NULL) {
    return;
  }
  if (img->palette != NULL) {
    free(img->palette);
  }
  for (i = 0; i < img->height; i++) {
    free(img->map[i]);
  }
  free(img->map);
  free(img);
}

クローン関数

画像を加工等する場合、現在の画像はそのまま保持して、別に用意したコピーを加工する。 という処理が必要になる場合があるだろう。 だが、ここで使用する画像データは内部でポインタを持っているため、いわゆる memcpy ではコピーが作れない。 そこで、 DeepCopy を行い、内部で参照しているポインタの先もまとめてコピーを行う、クローン関数を用意する。

image_t *clone_image(image_t *img) {
  uint32_t i;
  image_t *new_img = allocate_image(img->width, img->height, img->color_type);
  if (new_img == NULL) {
    return NULL;
  }
  new_img->palette_num = img->palette_num;
  if (img->color_type == COLOR_TYPE_INDEX) {
    memcpy(new_img->palette, img->palette, sizeof(color_t) * img->palette_num);
  }
  for (i = 0; i < img->height; i++) {
    memcpy(new_img->map[i], img->map[i], sizeof(pixcel_t) * img->width);
  }
  return new_img;
}

以上、C言語プログラミングで画像を扱う上でのデータ構造と、 それを使用開始するために必要な関数を作成した。 これだけではまだ何もできないが、処理を行うための下地を整えることができたということになる。