Hatena::ブログ(Diary)

昨日と今日とそれから明日

2008-09-29

エッシャーっぽい絵を生成する "escherize" を OpenCV で作ってみた

12:49

インスパイア元 --> 映像奮闘記: エッシャーっぽい絵を生成する「エッシャーくん」を作ってみた。

画像を変換してエッシャーっぽく無限渦巻きにする,というプログラム.面白そうだったので,OpenCV を使って作ってみました.というか,上のサイトの python で書かれていたコードをほとんどそのまま C++ に落としただけです.コードは記事の最後に載っけておきます.

変換のアルゴリズムの詳細はこちら --> ”A logarithmic image transformation”.要は,入力画像からドーナツ型の領域を切り取って,一ヶ所ハサミを入れて,渦巻き一周分になるようにぐねっと歪めて,拡縮を繰り替えしながら貼り付けていく,と.これを複素平面での単純な変換でやっているのがすごい.

変換結果は下のようになります.補間をしていないので,よく見るとちょっとジャギーな仕上がりです.

f:id:futashika:20080929123554p:image:w256

ちなみに,のコードはこちら.

#include <math.h>
#include <libgen.h>
#include <stdio.h>

#include <cxcore.h>
#include <cv.h>
#include <highgui.h>

/**
 * エッシャー風の画像に変換する
 * src と dst は同じサイズでなくても,同じタイプでなくても大丈夫
 * @param src 入力画像
 * @param dst 出力画像
 * @param r1  変換に使用する領域の内径
 * @param r2  変換に使用する領域の外径
 */
void escherize(CvMat *src, CvMat *dst, double r1, double r2) {
  CV_FUNCNAME("escherize");
  __BEGIN__

  double tw, th, alpha, f, ca, sa;

  // 引数を検査する
  if (r1 >= r2)
    CV_ERROR(CV_StsBadArg, "inner radius is too long or outer raidus is too short");
  if (r1 <= 0.0)
    CV_ERROR(CV_StsOutOfRange, "inner radius is too short or negative");
  if (r2 >= src->rows/2.0 || r2 >= src->cols/2.0)
    CV_ERROR(CV_StsOutOfRange, "outer radius is too long");

  // 計算に必要な定数を求める
  tw    = log(r2/r1);  // タイルの幅
  th    = M_PI * 2.0;  // タイルの高さ
  alpha = atan(tw/th); // タイルに変換する際の回転角度
  f     = cos(alpha);  // タイルに変換する際のスケール
  ca    = cos(alpha);
  sa    = sin(alpha);

  // 出力画像を生成する
  for (int j = 0; j < dst->height; ++j) {
    double dy = j - dst->height / 2.0;
    for (int i = 0; i < dst->width; ++i) {
      double dx = i - dst->width / 2.0;
      // 極座標に変換する
      double r = sqrt(dx*dx + dy*dy);
      if (r < 0.5) {
        cvSet2D(dst, j, i, cvScalarAll(0.0));
        continue;
      }
      double theta = atan2(dy, dx);
      // タイル空間に変換する
      double lr = log(r);
      double re = (lr *  ca + theta * sa) / f;
      double im = (lr * -sa + theta * ca) / f;
      // 中心のタイル範囲に収める
      while (re > tw ) re -= tw;
      while (re < 0.0) re += tw;
      while (im > th ) im -= th;
      while (im < 0.0) im += th;
      // 入力画像の座標に変換する
      double sx = r1 * exp(re) * cos(im) + src->width  / 2.0;
      double sy = r1 * exp(re) * sin(im) + src->height / 2.0;
      // 出力画像の画素値を求める
      // TODO: 補間したい
      cvSet2D(dst, j, i, cvGet2D(src, (int)sy, (int)sx));
    }
  }

  __END__
}

/**
 * 使い方を表示して,プログラムを終了する
 * @param status  終了時に親プロセスに返す値
 * @param command コマンド名
 */
void usage(int status, const char* command) {
  printf("Usage: %s src dst r1 r2\n", command);
  printf("Arguments:\n"
         "  src      source image file\n"
         "  dst      dest image file\n"
         "  r1, r2   transform parameters (r1 < r2 < image_size/2)\n");
  exit(status);
}

/**
 * メイン関数
 * @param argc コマンドライン引数の数
 * @param argv コマンドライン引数の配列
 * @retval 終了ステータス
 */
int main(int argc, char** argv) {
  CvMat *src = 0, *dst = 0;
  __BEGIN__

  double r1, r2;
  const char *src_file, *dst_file;

  // コマンドライン引数をパースする
  if (argc != 5)
    usage(EXIT_FAILURE, basename(argv[0]));
  src_file = argv[1];
  dst_file = argv[2];
  r1 = atof(argv[3]);
  r2 = atof(argv[4]);

  // 画像を読み込む
  src = cvLoadImageM(src_file, CV_LOAD_IMAGE_UNCHANGED);

  // 出力領域を確保する
  dst = cvCreateMat(src->rows, src->cols, CV_MAT_TYPE(src->type));

  // エッシャー風に変換する
  escherize(src, dst, r1, r2);

  // 出力画像を保存する
  cvSaveImage(dst_file, dst);

  __END__
  cvReleaseMat(&src);
  cvReleaseMat(&dst);
}

コンパイルと実行は下のような感じで.

$ g++ `pkg-config opencv --cflags` -o escherize escherize.cc `pkg-config opencv --libs` -lm
$ ./escherize lena.png lena_escherize.png 50 150
トラックバック - http://d.hatena.ne.jp/futashika/20080929/1222660182