画像データの取り扱い

作成:

前回は、画像を表現する方法を決めた。 今回は、画像の表現形式間の変換方法について解説する。

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

画像の表現形式の変換

前回までに、ひとつのピクセルの表現方法として、 RGB/RGBA/グレースケール/インデックスカラーという方式のいずれも表現できるデータ構造を考えた。 その表現形式をそのまま最後まで保持するだけなら別に気にする必要はないのだが、 様々な加工を行うことを考えると、それらの間の表現方法を変換することを考える必要が出てくる。 そのため、実際に画像の加工などを行う前に、その変換方法を解説しておく。

RGB→RGBA

まず、難しくないのは表現の幅が広がる方向の変換である。 RGB形式はRGBA形式で完全に表現できるため特に難しいことを考えずに変換することができる。 特にRGB形式を扱う場合に、アルファ値として不透明を設定しているという前提であれば、 画素情報には一切触れずに、color_typeの値を変更するだけでよい。

そのため、 image.c では、わざわざ独立した関数としては用意していない。 様々な形式をRGBAへ変換するという関数を用意しており、 RGB形式に変換した後は、color_typeの値をRGBAに書き換えるだけである。

image_t *image_to_rgba(image_t *img) {
  switch (img->color_type) {
    case COLOR_TYPE_INDEX:
      img = image_index_to_rgb(img);
      img->color_type = COLOR_TYPE_RGBA;
      break;
    case COLOR_TYPE_GRAY:
      img = image_gray_to_rgb(img);
      img->color_type = COLOR_TYPE_RGBA;
      break;
    case COLOR_TYPE_RGB:
      img->color_type = COLOR_TYPE_RGBA;
      break;
    case COLOR_TYPE_RGBA:
      break;
  }
  return img;
}

グレースケール→RGB

次に、同様に特に難しくない返還としてグレースケールからRGBへの変換である。 gの値をrgbにコピーするだけでよい。 共用体の性質上、rの値とgの値は同一のメモリを参照しているので再代入の必要性はないが、 分かりにくくなるので合わせて代入処理を書いている。 先に書いたように、RGBとRGBAが互換変換できるように、アルファ値として0xffを設定しておくようにする。

image_t *image_gray_to_rgb(image_t *img) {
  uint32_t x, y;
  if (img == NULL) {
    return NULL;
  }
  if (img->color_type != COLOR_TYPE_GRAY) {
    return NULL;
  }
  for (y = 0; y < img->height; y++) {
    for (x = 0; x < img->width; x++) {
      pixcel_t *p = &img->map[y][x];
      const uint8_t g = p->g;
      p->c.r = g;
      p->c.g = g;
      p->c.b = g;
      p->c.a = 0xff;
    }
  }
  img->color_type = COLOR_TYPE_RGB;
  return img;
}

インデックスカラー→RGB

インデックスカラーからRGBへの変換も単純な処理で済む。 インデックス値から色情報を参照し、その色情報をその画素に代入するだけである。 変換が終わると、カラーパレットは不要となるため、freeしておく。 注意点として、インデックス値であるicは共用体であるため、 cに代入を行った時点でiの値が変化してしまう。 以下の例では、式の評価順から影響はないが、 書き方によっては途中でiの値が変化してしまい正常に動作しない可能性があるので、 一度iの値を一度別の変数に保存してからその値を使って処理するようにした方がいいだろう。

image_t *image_index_to_rgb(image_t *img) {
  uint32_t x, y;
  if (img == NULL) {
    return NULL;
  }
  if (img->color_type != COLOR_TYPE_INDEX) {
    return NULL;
  }
  for (y = 0; y < img->height; y++) {
    for (x = 0; x < img->width; x++) {
      pixcel_t *p = &img->map[y][x];
      if (p->i >= img->palette_num) {
        return NULL;
      }
      p->c = img->palette[p->i];
    }
  }
  img->color_type = COLOR_TYPE_RGB;
  free(img->palette);
  img->palette = NULL;
  img->palette_num = 0;
  return img;
}

RGBA→RGB

さて、ここから面倒になる。 表現する情報量が減る方向の変換なので、何らかの方法で変換を行い情報量を減らす必要がある。 当然、情報を減らすので元に戻せない、不可逆な変換を行うことになる。

RGBAからRGBへ変換するということは、アルファ値をRGB値に織り込む処理が必要になる。 一瞬、アルファ値を全部不透明(0xff)にしちゃえばいいんじゃないか? と思ってしまう人もいるかもしれないが、そういう訳にはいかない。 全体が同一のアルファ値なら、まだある程度意味のあるものになるかもしれないが、 透過率がある色は、背景色と混ざることを前提としているため、 アルファ値を無視してしまうと全く違う色が現れてしまう。

例えば、以下のように、アルファチャンネルが適切に設定された画像は、 どのような背景の上においても綺麗に表示される。

アルファチャンネル有り Bugdroid ©Google CC BY 3.0

そして、このアルファチャンネルを全部不透明に書き換えるという処理を行った結果が以下である。 こういう結果を意図した人はいないはずだ。

アルファチャンネル無視 Bugdroid ©Google CC BY 3.0

RGBAからRGBに変換するには、アルファブレンドを行う必要がある。 そこで、変換関数にはひとつ引数を追加し、背景色を渡せるようにして、 その背景色とアルファブレンドを行うようにする。

image_t *image_rgba_to_rgb(image_t *img, color_t bg) {
  uint32_t x, y;
  if (img == NULL) {
    return NULL;
  }
  if (img->color_type != COLOR_TYPE_RGBA) {
    return NULL;
  }
  for (y = 0; y < img->height; y++) {
    for (x = 0; x < img->width; x++) {
      pixcel_t *p = &img->map[y][x];
      const uint8_t a = p->c.a;
      p->c.r = (p->c.r * a + bg.r * (0xff - a) + 0x7f) / 0xff;
      p->c.g = (p->c.g * a + bg.g * (0xff - a) + 0x7f) / 0xff;
      p->c.b = (p->c.b * a + bg.b * (0xff - a) + 0x7f) / 0xff;
      p->c.a = 0xff;
    }
  }
  img->color_type = COLOR_TYPE_RGB;
  return img;
}

アルファブレンドの計算がちょっと複雑なので解説する。 アルファ値が0~1の実数だとすると、画像:背景色をα:(1-α)の比率で加算すればアルファブレンド計算ができる。

しかし、ここではアルファ値は0~255の整数で保持していているので、アルファブレンドの計算を整数で行っている。 この整数演算で、というところは、プログラム上の整数演算の性質に慣れていないとちょっとむずかしいかもしれない。

画像のRGB値をα倍、背景色のRGB値を(0xff-α)倍して加算し、0xffで割っている。 先に掛け算を行い、後で割り算を行うことで、浮動小数点演算を行わずにすむ。 次に、+ 0x7fという値はどこから来たのかと思われるかもしれない。 これは小数点以下を四捨五入するために加えている。 整数演算では割ったあまりは切り捨てられてしまうが、割った結果0.5になる数字を予め割られる数に加えておくと、 あまりを切り捨てた結果は小数点以下を四捨五入したのと同じ結果が得られるという仕組みだ。 さらに厳密に言えば四捨五入に相当する処理(0x7f以下を切り捨て、0x80以上を繰り上げ)を行いたいのであれば 0x80を足すべきなのだが、他の処理との兼ね合いで0xff/2の値として0x7fにしている。

浮動小数点演算を使った場合でも、浮動小数点数を整数にキャストするときは小数点以下が切り捨てられてしまうので、 四捨五入を行いたい場合は0.5を加算してからキャストする、ということが行われる。

式の途中経過はuint8_t型をオーバーフローしているが、汎整数拡張によりint型で計算されるため問題ない。 また、最終的な値が255を超えてしまうとオーバーフローが発生するが、 この計算ではどのような値の組み合わせでも、255を超えないため、特にケアはしていない。

RGB→グレースケール

次はRGBをグレースケールに変換する方法である。 色情報を失わせて、明るさの情報だけを残すという変換になるのだが、 色の明るさはどのようにすれば取り出せるのだろうか?というところが課題となる。

明るさという定義には様々あるが、安直に思いつくものの一つとして、RGBの平均値を取る方法がある。 この方法でもある程度の変換を行うことは可能であるが、 平均値をとるということは、RGBの三原色全てを同じ明るさであるとみなしているということになる。 しかし、実際は青より緑のほうが明るいと感じるように、人が感じる明るさとは違ってしまう。

また、HSV(色相、彩度、明度)色空有間のV(明度)という指標では、RGBの値の最大値を取る。 しかし、最大値ということは、白も赤も緑も青も同じ明るさになってしまい、 RGBの平均値以上に人が感じる明るさとは違っている。

人の感じる明るさという指標で変換をかけるには、 三原色の中で緑は明るさに対する影響が大きく、青は小さい。 その辺を考慮した、輝度色差信号の輝度成分を取り出す計算式を利用する。

image_t *image_rgb_to_gray(image_t *img) {
  uint32_t x, y;
  if (img == NULL) {
    return NULL;
  }
  if (img->color_type != COLOR_TYPE_RGB) {
    return NULL;
  }
  for (y = 0; y < img->height; y++) {
    for (x = 0; x < img->width; x++) {
      pixcel_t *p = &img->map[y][x];
      const uint8_t r = p->c.r;
      const uint8_t g = p->c.g;
      const uint8_t b = p->c.b;
      // ITU-R BT.601規定の輝度計算で変換する
      const uint8_t gray = (uint8_t) (0.299f * r + 0.587f * g + 0.114f * b + 0.5f);
      memset(p, 0, sizeof(pixcel_t));
      p->g = gray;
    }
  }
  img->color_type = COLOR_TYPE_GRAY;
  return img;
}

輝度成分の変換方法にも複数あり、ここではコメントにも書いているが ITU-R BT.601の変換式を利用している。 SD解像度の動画などで利用されている方法である。 HD解像度の動画ではITU-R BT.709になり、少し変換式が異なる。

この説明をきちんとするには、もっと様々な知識が必要になるのだが、 残念ながら私は素人なので、この辺でご容赦願いたい。

以上、形式間の変換を説明した。 他にもあるがあとは、image.cを参照いただきたい。 RGBからカラーパレットへの変換では減色処理が必要になるが、 減色処理自体が一大テーマになってしまうので、まだ手を付けていない。 そのうち取り組んで完成したら解説したいと思う。