【無料】ProcessingでHDR画像作成ソフトを自作【夏休みの宿題に最適】

はじめに(安西先生、HDR画像を作りたいです)

 HDR!HDRがしたいよぅ!!!安西先生に三井が涙ながらにバスケへの思いを語るシーンに涙した人たちも、みなおっさんになってしまいました。そんなおっさんが突然HDRフォトをしたくてたまらなくなってしまって、結局ソフトを自作したので公開するよという話です。そうです、スラムダンクは一切関係ありません。

対象年齢

 幼児から大丈夫☆おっさんの趣味から、お子さんの夏休みの宿題まで幅広く使えますね☆ただし、基礎的なプログラミングと高校数学程度の知識が必要かも。まあよく分かんない人は、最悪最後に出てくるプログラムソースをコピペすればOK!大人は結果が全てなのさ!

先生!HDR画像って何ですか?

 いい質問ですね!HDRというのはハイダイナミックレンジ(High Dynamic Range)の略でその名の通り、ダイナミックレンジの広いという意味。普通PC上の写真とか画像とかのJPEGデータはRGBの0〜256までの8bit×3で表現されます。これは一般的なモニタの表示性能の限界から来ている訳です。このモニタ限界の範囲を超えた(ように見える)画像をなんとかを作ってやろうぜ!という試みで作られたのがHDR画像です。HDR画像が出来上がるまでのプロセスを簡単に図にすると以下のようになります。

f:id:karaage:20130929153106j:plain

 流れとしては、露出を変えた3枚の画像(明るめの画像、普通の画像、暗めの画像)を「HDR合成」と呼ばれる手法を用いることで8bit×3を超越したHDRImageとよばれるものを作ってやります(※上の図のHDRImageのビット数間違ってたので直しました)。ここで、合成する画像の数によっていくらでも大きいダイナミックレンジの画像を作ってやることができるのですが、このままではモニタの性能を超えているので表示できません。そのためモニタで表現できる8bit×3まで情報量を落としてやる処理が必要となります。この手法が「トーンマッピング」と呼ばれ、調べたら様々な手法が提案されていて、この手法が研究の対象や特許になったりする結構奥深い分野みたいです。

 まとめると、HDR画像は「HDR合成」と「トーンマッピング」という2つのプロセスによって、高いダイナミックレンジを表現する画像のことです。結構この2つを一緒に自動で処理するソフトが多いので、この2つの区別がついてなかったり、間違って理解している人もいるようなので注意して下さい。ここテストに出ますよ!

「HDR合成」と「トーンマッピング」の仕組み

 HDR合成ですが、最初は単純に足し算すればOKだろ!と思っていたのですが、それだとあんまりうまくいかなかったです。日本の文献が無くて、海外の論文とか流し読みしたところ、どうも露出に応じて補正した上で加算してやる必要があるみたいです。とりあえず今回は露出に応じて適当にガンマ補正してやることにしました。多分ここの補正も色々ノウハウがあると思うのですが、まだそこまで深入りできていません。あ、ガンマ補正分からない人は適当にグーグルさんに聞いて下さい☆ただの補正でそんな難しいことはないです。
 その後トーンマッピングです。大きくわけるとグローバルとローカルに分かれるみたいで、画像全体の輝度の平均値求めて、その値を元に情報量落としてやるのがグローバル。多分iPhoneはこっちの手法つかっています。ローカルは、ピクセル毎にある程度の範囲の周囲の輝度の平均値を求めて、その値を元に情報量落とすというのを全ピクセルに対して都度行う方式で、Photomatixとかのソフトで作られるいかにもHDR画像と呼ばれる絵画調に見えるHDR画像はローカルで作られています(多分)。実はちょっと自信ないので、間違ってたら誰か指摘して下さい。
 トーンマッピングに関して簡単に図にまとめると以下のようになります。

120725_HDRPhoto02
 わ、わかるよね??

 輝度の平均値の求め方は、一番メジャらしい対数平均輝度を用いました。ここは、一番有名どころの論文の数式どおりではどうしてもうまくいかなかったのではまりました。とりあえず論文が間違っていると結論付け、式を組み直しました。

成果物

 これらの手法をプログラムに実装して実際に作ってみたHDR写真です。まずは元画像となる、露出を変えた3つの写真。

120725_HDRUnder
 露出アンダーの写真

120725_HDRProper
 露出適正の写真

120725_HDROver
 露出オーバーの写真

 そして、これらをHDR合成してトーンマッピングしたHDR写真がこれだ!

screenshot_HDRTONE3

 結構それっぽいのではないでしょうか?Olympusのデジ一にはドラマチックトーンというHDR風のフィルタがあって、それを狙ってパラメータチューニングしてみました。

120725_HDRDramaticTone
 ちなみに、これがドラマチックトーンをかけた同じ画像。

 うん、ドラマチックトーンの方が全然よいですね。まあ、色々パラメータ弄ったりアルゴリズム変えて画像を好きに弄れるのが自作ソフトの強みということで!

これから

 気が向いたらアルゴリズムやパラメータ改良して、色んな写真で試してみようかなと。目指せ追い越せPhotomatix!ドラマチックトーン!みなさんも素敵なHDR画像ができたら教えて下さいね☆

ソースコードと使い方

 ソースコード公開します。いつもの通りProcessingというプログラム言語使っています。手っ取り早く無料HDR画像作成ソフト欲しい人は、ProcessingインストールしてこれをコピペしてRUNして下さい。論文等の内容しか使ってないので、特許とかは特に大丈夫なはず。
 実行すると、ファイルダイアログが開くので、暗めの写真、普通の写真、明るめの写真の順に読み込ませて下さい。しばらく待つとHDR画像が出来上がるので、「p」ボタンを押すと保存できます。スペースキーで終了です。

 パラメータは scopeがトーンマッピングのとき、輝度求めるのに使う周囲のピクセルの範囲です。scopeを大きくすると指数関数的に処理時間が大きくなるので、その場合は、scope_speedとaverage_speedの2つを大きくすると高速化できます。scope_speedは1からscopeの半分以下の値で、average_speedは0から画像の横サイズまでの大きさで指定して下さい。aはトーンマッピングに使用するパラメータで大きくするほど明るくなります。理論的には0.18が標準らしいです。
 gamma_u, gamma_n, gamma_oはそれぞれHDR合成するときの、暗めの写真、普通の写真、明るめの写真に対するガンマ補正値です。好きに弄って下さい。color_gainは色のゲインです。お好みで設定ください。とりあえず1にしておくのがよいかもしれません。

PImage img0;
PImage img1;
PImage img2;

float lum;
float lum_sum;
int sum_numb;

final int scope = 50; // 1 to picture width
final int scope_speed = 5; // 1 to scope
final int average_speed = 2; // 0 to picture width
final float a = 0.27;
final float gamma_u = 0.5;
final float gamma_n = 1;
final float gamma_o = 0.5;
final float color_gain = 3.2;
final float delta = 0.01;

float[] lut_u = new float[256];
float[] lut_n = new float[256];
float[] lut_o = new float[256];

void setup() {
  size(100, 100);

  println("select under exposed photo.");
  String imgPath = selectInput();
  img0 = loadImage(imgPath);

  println("select proper exposed photo.");
  imgPath = selectInput();
  img1 = loadImage(imgPath);

  println("select over exposed photo.");
  imgPath = selectInput();
  img2 = loadImage(imgPath);

  size(img0.width, img0.height);

  for (int i = 0; i < 256; i++){
    lut_u[i] = 255*pow(((float)i/255),(1/gamma_u));
  }

  for (int i = 0; i < 256; i++){
    lut_n[i] = 255*pow(((float)i/255),(1/gamma_n));
  }

  for (int i = 0; i < 256; i++){
    lut_o[i] = 255*pow(((float)i/255),(1/gamma_o));
  }
}

void draw(){
  image(img0, 0, 0);

  // Making HDRImage-----
  float[] hdr_img_r = new float[img0.height*img0.width];
  float[] hdr_img_g = new float[img0.height*img0.width];
  float[] hdr_img_b = new float[img0.height*img0.width];

  img0.loadPixels();
  img1.loadPixels();
  img2.loadPixels();

  for(int i = 0; i < img0.width*img0.height; i++){
    color tmp_color0 = img0.pixels[i];
    color tmp_color1 = img1.pixels[i];
    color tmp_color2 = img2.pixels[i];

    hdr_img_r[i] =
      lut_u[(int)red(tmp_color0)] + lut_n[(int)red(tmp_color1)] + lut_o[(int)red(tmp_color2)];
    hdr_img_g[i] =
      lut_u[(int)green(tmp_color0)] + lut_n[(int)green(tmp_color1)] + lut_o[(int)green(tmp_color2)];
    hdr_img_b[i] =
      lut_u[(int)blue(tmp_color0)] + lut_n[(int)blue(tmp_color1)] + lut_o[(int)blue(tmp_color2)];

    hdr_img_r[i] = hdr_img_r[i]/3*color_gain;
    hdr_img_g[i] = hdr_img_g[i]/3*color_gain;
    hdr_img_b[i] = hdr_img_b[i]/3*color_gain;
  }
  //----Making HDRImage

  //ToneMapping----
  color[] tmp_img = new color[img0.height*img0.width];
  int tmp = average_speed;
  float lum_sum_w = 0;

  for(int y = 0; y < img0.height; y++){
    tmp = average_speed;
    for(int x = 0; x < img0.width; x++){
      int pos = x + y*img0.width;
      lum_sum = 0;
      sum_numb = 0;
      tmp++;
      if(tmp > average_speed){
        tmp = 0;
        for(int y_2 = y-scope; y_2 < y+scope; y_2 += scope_speed){
          for(int x_2 = x-scope; x_2 < x+scope; x_2 += scope_speed){
            if(y_2 >= 0 && y_2 < img0.height && x_2 >=0 && x_2 < img0.width){
              if(sqrt((x_2-x)*(x_2-x)+(y_2-y)*(y_2-y)) <= scope){
                sum_numb++;
                int pos_2 = x_2 + y_2*img0.width;
                lum_sum += log((0.3*hdr_img_r[pos_2] + 0.59*hdr_img_g[pos_2] + 0.11*hdr_img_b[pos_2])/256+delta);
              }
            }
          }
        }
        lum_sum_w = exp(lum_sum/(float)sum_numb);
      }

      float lum = 0.3*hdr_img_r[pos] + 0.59*hdr_img_g[pos] + 0.11*hdr_img_b[pos];
      float u = -0.17*hdr_img_r[pos] - 0.33*hdr_img_g[pos] + 0.5*hdr_img_b[pos];
      float v = 0.5*hdr_img_r[pos] -0.42*hdr_img_g[pos] - 0.08*hdr_img_b[pos];
      float lum_w = lum/lum_sum_w*a;
      float r = lum_w + 1.4*v;
      float g = lum_w -0.34*u -0.71*v;
      float b = lum_w + 1.77*u;
      
      tmp_img[pos] = color(r,g,b);
    }
  }

  for(int y = 0; y < img0.height; y++){
    for(int x = 0; x < img0.width; x++){
      int pos = x + y*img0.width;
      set(x, y, tmp_img[pos]);
    }
  }
}


void keyPressed() { 
  // save image
  if(key == 'p' || key == 'P') {
    save("screenshot.jpg"); 
    println("screen saved."); 
  }

  // exit
  if(key == ' ') {
    exit();
  }
}

参考

HDR合成に関する論文
 斜め読みしただけです。HDR合成は足し算しただけでは駄目と気づかせてくれました。

トーンマッピングの論文
 一番上のリンクで論文が読めます。英語

日本語の論文
 上記の論文をベースに少し工夫した手法を論文にしているっぽいです。