Entry

プログラミングメモ - C++ で多態的な画像クラスを作る

2010年05月03日

作ってみたらうまくいったのでメモ。ただ,ちと規模が大きくなっちゃったので,ソースをすべて載せることができません。デバッグもまだ十分じゃないので,ここでは,やろうと思えばできるよ,ということで。ヒントだけ書いておきます。

多態的な画像クラスというのは,画像の持ち方や読み込み方/書き込み方が変わる場合でも,構成するクラスの内容を変更すること無しに機能を拡張したり,縮減したりする仕組みのことです。画像クラスなんつのは,画像処理に欠かせない一般的なクラスである一方,開発者が10人いたら10種類のクラスができちゃうくらい車輪の再発明が盛んなクラスだったりします。んなもんで,リファレンスはたくさん公開されている。しかし,多態的なクラスはあまり見ることがありません。Intel IPP の UIC(Unified Image Codec)は,割と多態的だった覚えがあるけれど,IPP がそもそも特殊なので,汎用的とはいえません。つことで,作るにはちと工夫しないといけません。

画像クラスには,例えば,白黒二値でピクセルの行列を保持するモノもあれば,8ビットのグレースケールで保持するものもあります。当然,フルカラーの場合もあるだろうし,アルファチャンネルを持っていたいこともある。フーリエ変換のような複雑な数値計算処理を行う場合は,各ピクセルを浮動小数点型の複素数で持っていたいところ(ピクセルごとに実数部と虚数部を持つ必要もないんだけれど)。

一方,画像クラスを中心とするクラス群で必要不可欠な機能の中に,ファイル入出力の機能があります。で,これまた種類がたくさんある。おなじみの画像フォーマット,たくさんありますよね。

こうした複数種類のデータ形式に対して,複数種類の読み書き方法を適用するには,どうすればいいでしょう。あたしが見た中で,最も最悪の実装は,次のような画像クラスです。

class Image8u {
public:
  Image() throw ();
  Image(int width, int height) throw (std::exception);
  Image(const Image& other) throw (std::exception);
  virtual ~Image() throw ();
public:
  Image& operator=(const Image& rhs) throw (std::exception);
public:
  bool alloc(int width, int height) throw (std::exception);
  bool clear() throw ();
  bool isAlloc() const throw ();
public:
  bool readBMP(const std::string&) throw(std::exception);
  bool readPNG(const std::string&) throw(std::exception);
  bool readJpeg(const std::string&) throw(std::exception);
  bool readTIFF(const std::string&) throw(std::exception);
  //  :
  bool writeBMP(const std::string&) throw(std::exception);
  bool writePNG(const std::string&) throw(std::exception);
  bool writeJpeg(const std::string&) throw(std::exception);
  bool writeTIFF(const std::string&) throw(std::exception);
  // :
private:
  int width_;
  int height_;
  int step_;
  unsigned char* buf_;
};

すべての読み書きクラスを,すべての画像クラスに実装するわけです。これはありえない。ここでは,8ビットのグレースケール画像を読み込むクラスだけを書いたけれども,同じようなクラスがたくさんあるんです。もちろん,読み込みメソッドは,そのクラスごとに書かれている。こんなことしてると,例えば,サポートする画像ファイルが1つ増えるだけで,すべての画像クラスに読み書きのメソッドを追加することになるわけで,大工事になってしまいます。当然,反対に,画像クラスの種類が増えた場合も大工事になる。

つことで,考えたわけですけれど,これ,結局やらなきゃいけないのは次の点なんだと思います。

  • 画像の読み書きメソッドを画像クラスそのものから分離する。
  • デフォルトのピクセル型を定義する。
  • 画像クラス一般を(内部バッファの型も含めて)抽象化する。

まず,最初の点として,画像の読み書きメソッドは,クラスとして分離します。つまり,JpegReader や PNGWriter のようなクラスを作るというわけです。読み込みクラスは,読み込み時にファイルの内容をいったん「デフォルトのピクセル型」に変換します。そして,変換されたデフォルト型の値を画像クラスに読ませて,内部バッファに合うようにセットするわけです。もちろん,この際,「デフォルトのピクセル型」を通じたインターフェイスは,画像クラスがみんな持っているメソッド(純粋仮想関数)としておく。読み込み時,ファイルのピクセル値をデフォルトのピクセル型に翻訳するのは,読み書きクラスの責任です。そして,受け取ったデフォルトのピクセル型の値を,内部バッファに合うように収める責任は,画像クラスにあります。

デフォルトのピクセル型は,例えばこんな具合に表せます。

  class PixelQuad {
  public:
    PixelQuad() throw ();
    PixelQuad(const PixelQuad& other) throw ();
    virtual ~PixelQuad() throw ();
  public:
    PixelQuad& operator=(const PixelQuad& rhs) throw ();
  public:
    void r(UInt8 r) throw ();
    void g(UInt8 g) throw ();
    void b(UInt8 b) throw ();
    void a(UInt8 a) throw ();
    void gray(UInt8 blightness) throw ();
    UInt8 r() const throw ();
    UInt8 g() const throw ();
    UInt8 b() const throw ();
    UInt8 a() const throw ();
    UInt8 gray() const throw ();
  private:
    UInt8 r_;// red
    UInt8 g_;// green
    UInt8 b_;// blue
    UInt8 a_;// alpha channel
  };

ここでは,アルファチャンネルつきの 32bit 画像にしました(実質は24bit)。で,JpegReader クラスの読み書きは,こんな感じなる(libjpeg を使った読み込みの場合)。

void
read(const std::string& filename,  Image* image) {
  // :
  PixelQuad pixel;
  while (cinfo.output_scanline < cinfo.image_height) {
    Int32 y = cinfo.output_scanline;
    jpeg_read_scanlines(&cinfo, &row, 1);
    for (UInt32 x = 0; x < cinfo.image_width; ++x) {
      // 汎用型で読む
      pixel.r(scanLines.at((x * 3) + 0));
      pixel.g(scanLines.at((x * 3) + 1));
      pixel.b(scanLines.at((x * 3) + 2));
      pixel.a(0);
      // 汎用型を渡す
      image->set(x, y, pixel);
    }
  }
  // :
}

いったん汎用型をかませることで,どんな形式のファイルでも PixelQuad クラスで正規化されることになります。image->set() 関数で受ける段階では,Jpeg でも BMP でも PNG でも,画像フォーマットの違いはありません。もっとも,この方法は,読み書きに時間がかかるので,メモリアクセスのオーバーヘッドよりも大きなオーバーヘッド(ファイルIO)のあるファイルの入出力や,画像フォーマットの交換くらいにしか使えないんじゃないかと思います。実際に画像処理をする場合は,各画像クラスに固有の速いメソッドを定義すべきです。

ともかくも,いったん汎用型をかませることで,どんな形式のファイルでも PixelQuad クラスで正規化されることになります。画像クラスとしては,どんなファイルが来ても同じ読み方で読めるので,フォーマットが増えるたびにメソッドを増やすような愚かなことはしなくて済みます(読み書き用のクラスを1つづつ作るだけでいい)。

さて,これで読み書き方法が複数ある件については解決です。それでは,画像の種類(保持方法)が異なる件についてはどうでしょう。これには,継承(仮想関数)とテンプレートを使います。

大体こんな感じの継承関係を作ります。

class Image {
  virtual void set(int x, int y, const PixelQuad& color) = 0;
  virtual const PixelQuad& get(int x, int y) const = 0;
};

template<typename T>
class ImageAdapter : public Image {
  virtual void set(int x, int y, const PixelQuad& color) = 0;
  virtual const PixelQuad& get(int x, int y) const = 0;
};

class Image8u : public ImageAdapter<unsigned char> {
  virtual void set(int x, int y, const PixelQuad& color) { /* ここで実装 */ };
  virtual const PixelQuad& get(int x, int y) const { /* ここで実装 */ };
};

ファイルのやり取りにおいては,一番基底にあるクラス Image 型のポインタ(や参照)を通じて行うことにします。上の Jpeg 読み込み部でもそうだけれども,read メソッドは仮想関数なので,基底クラスのポインタで受けてそのメソッドを呼んだら,生成時のクラス(派生クラス)のメソッドを呼ぶことができます。関数の呼び先では,Image8u のオブジェクトが来たのか,Image32u のオブジェクトが来たのか,はたまた ImageComplex のオブジェクトが来たのか,分かりません。分かっている必要がないんですね。

つことで,JpegReader の仕事は,ピクセルをファイルから読み取ってから PixelQuad を作って(正規化して),何者かは分からないけどやってきた image なるオブジェクトの read() という関数に作ったそれを渡すだけ,という単純なものになります。

こうした,継承と仮想関数の多態的な用法は,使いこなすとかなり便利です。初心者さんには混乱の元になるかもしれませんけど。

最後に,内部バッファの型を吸収(抽象化)する方法についても触れておきます。これは継承関係を見れば明らかなんですけれど,テンプレートを使っています。本当だったら,このクラスはいらなくて,派生クラス(Image8u)だけでもいいんですけれど,型の管理は外に出しておいた方が,後々バッファの管理で重複したコードを書かなくて済むだろうという判断があります。ここは,ちと思いつきなところがあるので,今考えるとなくてもよかったかな,とか思ったり。ま,せっかくだからこのままで。

画像の読み書きクラスについて基本的な構造ができたので,あとは対応できるフォーマットの数を増やしていくだけです。増産体制が整いましたです。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN