無料のHDRPhoto作成ソフト HDRHolic For Mac/For PCをリリースしました

HDR Photoって何?

 一般的には暗い写真と普通の写真と明るい写真と露出が異なる同じ構図の写真3枚を合成して、いい感じの写真をつくっちゃおうというものです。iPhoneでも4当たりのカメラから標準装備されています。あれは、3枚異なる露出で連写した後iPhone内で合成しているのです。
 そのHDR Photoつくる方法も色々あって、方法によっては非現実的・幻想的な写真ができたりするので結構人気がある分野だったりします。

HDRHolic For Mac/For PCを作った理由

 この手の分野ではPhotomatixというソフトが有名なのですが、ちょっとお高いのと色々調べたら自分でも作れそうだということで、以前Processingというプログラム言語で作成してソースを公開したりしています。『【無料】ProcessingでHDR画像作成ソフトを自作【夏休みの宿題に最適】

 ただ、あんまりにも使い勝手悪いから自分含めて誰も使ってないのと、もうちょっと真面目に突き詰めてみたい気持ちがあったのと、一度くらい自作ソフトを公開してみたいと思い作ってみました。
 HDRHolicという名前は、実は私が以前公開したソースを元に@towa_macerさんがiPhone版を実装してリリースされているのですが、そのネーミングとアイコンが非常に気に入っていましたので、今回ご本人の了承を得て名前とアイコンを使わさせていただきました。

 結構古い人間なので、フリーソフトといえばVectorだろ!という思いがあったのですが、なんか申請したものの音沙汰もないし、時間もかかりそうだし、時代はオープンソースだろ!というわけでSourceforge.jpでオープンソースソフトウェアとして公開することにしました。オープンソースなので、ライセンスの関係上ソースコードも公開しないといけないということでgit覚えたりと大変でした…気が向いたらここらへんのことも書くかもしれません。

HDRHolicをダウンロード

 ダウンロードは以下からお願いします
http://sourceforge.jp/projects/hdrholic/releases/

Macの人は「HDRHolic_For_Mac_v0_1.zip」をWindowsの人は「HDRHolic_For_PC_v0_1.zip」をダウンロードして下さい。

HDRHolicを使用する

 以下のページを充実していく予定です(予定は未定)。あんまり大きい画像を使うと落ちてしまうので、ほどほどの大きさの写真でためして下さい。

http://sourceforge.jp/projects/hdrholic/howto/usage

 多分Java無いと動かないので、動かない人は「java.com」でJavaをインストールして下さい。

スクリーンショット


 こんな感じ

ソースコード

 Processingで作っています。一気にMac版、PC(Windows0版つくれるのがマルチプラットフォームのProcessingのよいところですね。PC版の方のアイコンの設定の仕方は調べたのですがわかりませんでした。

 以下から見れます。git使えます。ちなみにSourceforge.jpにはプログラミング言語にProcessingが無かったけど登録してよかったのだろうか…全体的に適当なので不安感が残っています。

http://sourceforge.jp/projects/hdrholic/scm/git/HDRHolic/

 一応ブログにもはっつけておきます。今回はControlP5というライブラリ使ってコントロールウィンド追加しました。

import controlP5.*;

ControlP5 cp5;
ControlWindow controlWindow;
ControlWindow viewWindow;
Textlabel readmeText;

PImage img0;
PImage img1;
PImage img2;
PImage writeImg;

float scope_ratio = 5; // 1 to 100%

final int low_scope_speed_ratio = 20;
final float low_average_speed_ratio = 0.1;

final int high_scope_speed_ratio = 5;
final float high_average_speed_ratio = 0.1;

int scope_speed_ratio = low_scope_speed_ratio; // 1 to scope
float average_speed_ratio = low_average_speed_ratio; // 0 to picture width

final int max_lum_class = 30; // 1 to 256
int class_th = 15; // class threshold
float a_value = 0.27;
float gamma_u = 0.5;
float gamma_n = 1;
float gamma_o = 0.5;
float color_gain = 1;
final float delta = 0.01;
float[] lut_u = new float[256];
float[] lut_n = new float[256];
float[] lut_o = new float[256];

// hdr image
float[] hdr_img_r;
float[] hdr_img_g;
float[] hdr_img_b;

//Window Size
int size_x = 1024;
int size_y = 768;
int view_width, view_height;

void setup(){
  size(size_x, size_y);

  cp5 = new ControlP5(this);

  controlWindow = cp5.addControlWindow("controlP5window", 100, 100, 360, 600)
    .hideCoordinates()
    .setBackground(color(40))
    ;

  cp5.addTextlabel("guide")
      .setText("Guide:")
      .setPosition(40,40)
      .setColorValue(0xffffffff)
      .setFont(createFont("Georgia",20))
      .moveTo(controlWindow)
      ;

  readmeText = cp5.addTextlabel("label")
               .setText("Select an under exposed photo.")
               .setPosition(40,80)
               .setColorValue(0xffffffff)
               .setFont(createFont("Georgia",18))
               .moveTo(controlWindow)
                ;

  cp5.addSlider("gamma_u")
     .setRange(0, 2)
     .setPosition(40, 140)
     .setSize(200, 29)
     .moveTo(controlWindow)
     ;

  cp5.addSlider("gamma_n")
     .setRange(0, 2)
     .setPosition(40, 180)
     .setSize(200, 29)
     .moveTo(controlWindow)
     ;

  cp5.addSlider("gamma_o")
     .setRange(0, 2)
     .setPosition(40, 220)
     .setSize(200, 29)
     .moveTo(controlWindow)
     ;

  cp5.addSlider("a_value")
     .setRange(0, 0.5)
     .setPosition(40, 300)
     .setSize(200, 29)
     .moveTo(controlWindow)
     ;

  cp5.addSlider("color_gain")
     .setRange(0, 5)
     .setPosition(40, 340)
     .setSize(200, 29)
     .moveTo(controlWindow)
     ;

  cp5.addSlider("scope_ratio")
     .setRange(0, 20)
     .setPosition(40, 380)
     .setSize(200, 29)
     .moveTo(controlWindow)
     ;

  cp5.addSlider("class_th")
     .setRange(0, 30)
     .setPosition(40, 420)
     .setSize(200, 29)
     .moveTo(controlWindow)
     ;

  cp5.addButton("Save Image")
     .setPosition(40,500)
     .setSize(100,39)
     .moveTo(controlWindow)
     ;

  cp5.addButton("Exit")
     .setPosition(160,500)
     .setSize(100,39)
     .moveTo(controlWindow)
     ;

  String imgPath = selectInput();
  img0 = loadImage(imgPath);
  readmeText.setText("Select a normal exposed photo.");

  imgPath = selectInput();
  img1 = loadImage(imgPath);

  readmeText.setText("Select an over exposed photo.");

  imgPath = selectInput();
  img2 = loadImage(imgPath);

  writeImg = createImage(img0.width, img0.height, RGB);

  MakeHDR();
  ToneMapping();

  readmeText.setText("Completed.");

  if(img0.width > size_x || img0.height > size_y){
    float k_width = img0.width / size_x;
    float k_height = img0.height /size_y;
    float k_max;
    if(k_width > k_height){
      k_max = k_width;
    }else{
      k_max = k_height;
    }
    view_width = (int)(img0.width/k_max);
    view_height = (int)(img0.height/k_max);
  }else{
    view_width = img0.width;
    view_height = img0.height;
  }
}

public void controlEvent(ControlEvent theEvent) {
  if(theEvent.isFrom("color_gain")) {
    MakeHDR();
  }

  if(theEvent.isFrom("gamma_u")) {
    MakeHDR();
  }

  if(theEvent.isFrom("gamma_n")) {
    MakeHDR();
  }

  if(theEvent.isFrom("gamma_o")) {
    MakeHDR();
  }

  if(theEvent.isFrom("Save Image")) {
    String imgPath = selectOutput();
    writeImg.save(imgPath);
  }

  if(theEvent.isFrom("Exit")) {
    exit();
  }
}


void draw(){
  //  MakeHDR();
  ToneMapping();
  image(writeImg, 0, 0, view_width, view_height);
}

void MakeHDR(){
  hdr_img_r = new float[img0.height*img0.width];
  hdr_img_g = new float[img0.height*img0.width];
  hdr_img_b = new float[img0.height*img0.width];

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

  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));
  }

  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;
  }
}

void ToneMapping(){
  float lum_sum;
  int sum_numb;

  int scope = (int)(sqrt(img0.height*img0.width) * scope_ratio/100);
  int scope_speed = (int)(scope * scope_speed_ratio/100)+1;
  int average_speed = (int)(sqrt(img0.height*img0.width) * average_speed_ratio/100);

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

  float[] lum = new float[img0.height*img0.width];
  float[] lum_local = new float[img0.height*img0.width];
  int[] lum_class = new int[img0.height*img0.width];
  float[] u = new float[img0.height*img0.width];
  float[] v = new float[img0.height*img0.width];

  for(int y = 0; y < img0.height; y++){
    for(int x = 0; x < img0.width; x++){
      int pos = x + y*img0.width;
      lum[pos] = 0.3*hdr_img_r[pos] + 0.59*hdr_img_g[pos] + 0.11*hdr_img_b[pos];
      lum_local[pos] = log((0.3*hdr_img_r[pos] + 0.59*hdr_img_g[pos] + 0.11*hdr_img_b[pos])/256+delta);
      u[pos] = -0.17*hdr_img_r[pos] - 0.33*hdr_img_g[pos] + 0.5*hdr_img_b[pos];
      v[pos] = 0.5*hdr_img_r[pos] -0.42*hdr_img_g[pos] - 0.08*hdr_img_b[pos];
    }
  }
  
  int max_lum = (int)max(lum);
  for(int y = 0; y < img0.height; y++){
    for(int x = 0; x < img0.width; x++){
      int pos = x + y*img0.width;
      lum_class[pos] = (int)(lum[pos]/((max_lum+1)/max_lum_class));
    }
  }

  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){
                int pos_2 = x_2 + y_2*img0.width;
                if(abs(lum_class[pos] - lum_class[pos_2]) < class_th){
                  sum_numb++;
                  lum_sum += lum_local[pos_2];
              }
            }
          }
        }
        lum_sum_w = exp(lum_sum/(float)sum_numb);
      }

      float lum_w = lum[pos]/lum_sum_w*a_value;
      float r = lum_w + 1.4*v[pos];
      float g = lum_w -0.34*u[pos] -0.71*v[pos];
      float b = lum_w + 1.77*u[pos];
      
      writeImg.pixels[pos] = color(r,g,b);
    }
  }
  writeImg.updatePixels();
}