聞きかじりめも

主にC++やメディア処理技術などに関して気付いたことを書いていきます.ここが俺のメモ帳だ!

虹色ペンライトを改造

今年の初めに作ってからというものずっと放置していましたが,ようやく記事にする気になりました. 一応身内には制作過程の資料を公開してたのですが...

組み込みプログラミングの部分はかなり端折っているので,この記事を読めば誰でもすぐに作れるって訳ではないです. 興味ある人は参考程度に見ておいてください.

(20181116追記) 4thライブ仕様の新しいAqoursラブライブレードがこれとほぼ同じ実装らしいです。なんか報われた気がします。 (20211021追記) 諸事情により動画のアップロード先を書き換えました.また,Amazon広告を変えました.

問題提起

近年のライブシーンにおいて,多色LEDペンライトを使用する機会が増えている.これは演者を応援するグッズである以上に,観客が色によって能動的に情報発信するツールとしても使用される.観客自体が訓練し一種の舞台装置となる例[1]もあるが,特定のメンバーを応援するためにメンバーのイメージ色を表示する場合が多い.しかしキングブレードに代表される現在市販の「虹色」ペンライトの殆どは光源が一つであるため,光の色が変化していく時間的な虹色発光は可能であるものの,空間的な虹色発光が原理上不可能である.このため,同時に2人以上のメンバー色を表示し応援することが不可能という問題点があった.筆者は,μ's全員を同時に応援することを目的として,最大9色まで同時発光が可能なペンライト「レイン棒」を新規開発し,これらの問題への解決を試みた.本稿ではその製作過程を報告する.(論文のAbstract風に)

[1] 水樹奈々田村ゆかりのライブ会場が好例.彼らは高度に統率されており,群体として一つの大きな光のパフォーマンスを見せる時がある.最近ではサザンオールスターズが,WiFi技術を駆使して訓練なしに観客の光を制御する仕掛けをした.

つまりどういうことだってばよ

f:id:Mzawa2:20151010105742j:plain:w320

こんなのを作った.以上.

材料

外側

  • ボタン付きペンライト

ガワだけ使うのでボタンがついてりゃなんでもいいです.なんなら3Dプリンタで作ればもっと自由なデザインになります.私は安さ重視で「キングサンダーIK1」というキンブレの中華コピー品を使用しました(2015/01現在在庫・再入荷予定なし).ボタンスイッチは1個だけですが充分です.

f:id:Mzawa2:20151010114157j:plain:w320

電子パーツ

f:id:Mzawa2:20151010115513j:plain

  • LEDモジュール

秋月電子通商AE-WS2812Bを使用しました.モジュール自体は最近流行りのAdafruit NeoPixelです.普通の3色LEDではなく,信号線を直列に繋ぐだけで色制御が可能な特殊モジュールです.

(参考: フルカラーシリアルLED NeoPixelを試してみた - The jonki

DigiSparkという超小型Arduino互換ボードを使用しました.海外からの輸入品ですが,1個895円とお買い得です.初心者にも扱いやすいArduinoベースのマイコンなので,開発環境構築が非常に楽です.これくらいの大きさならペンライト内にすっぽりと収まるでしょう.私は収まりませんでした.

(参考: 橋本商会 » 895円の超小型Ardunoクローン DigiSparkを買った

最近は輸入代行業者でも買えるみたいです.

http://www.elefine.jp/SHOP/96285/103406/list.html

  • その他パーツ

DigiSparkと同じ大きさのユニバーサル基板(DigiSparkと一緒に買える).導線,5kΩくらいの抵抗(あれば),半田,ピンソケット,ピンヘッダ,その他工具.プッシュスイッチは元々ペンライトにあったものを流用.

製作方法

1.ペンライトを分解する

f:id:Mzawa2:20151010153245j:plain:w300

分解してみると,発光部根元の小さいLEDを透明アクリル板とビニール製の筒で拡散させて全体を光らせるという安価な実装でした.この筒にLEDのタワーを入れていきます.案外内部スペースが小さいので,基板実装は工夫が必要です.

2.LEDモジュールをはんだ付けする

f:id:Mzawa2:20151010153456j:plain:h300 f:id:Mzawa2:20151011150645j:plain:h300

まず1個目にピンヘッダをつけた後,LEDモジュールの6個の足のうち外側の4個(GND, VDD)を,先程の1個目の上にモジュールの向きを揃えてタワー状に半田づけします.タワーの高さと間隔はペンライトの大きさから考えましょう.次に,1個目のDOピンから2個目のDIピン,2個目のDOから3個目のDI…と9個目のDIまで繋げます. ここの半田付けはクソ面倒ですが根気よく行きましょう.

3.周辺回路を組み立てる

f:id:Mzawa2:20151010153652p:plain:h300

制御マイコンの2階を作る感じで,電源・スイッチ・LED信号線(根元の方のDI)・GNDをそれぞれ,マイコン電源(LED電源)・マイコンIOピン2箇所・電池の−極に繋げます.抵抗が手元になければ最悪省略してしまっても動きます.頑張って半田付けしましょう.

4.基板をペンライト内に納める

f:id:Mzawa2:20151010153943j:plain:h300 f:id:Mzawa2:20151010154010j:plain:h300

試しに作った制御回路を嵌め込んでみると,狭すぎて入らないことがあります.こういう時は思い切って本体を削ったり穴を開けたりしてみましょう.入るようになるかもしれません.

5.LEDタワーをセットする

f:id:Mzawa2:20151010154458j:plain:h300

先ほど製作したLEDタワーをビニール筒の中に納めます.拡散用アクリル板はいらないのでポイしてください.

6.サンプルプログラムを書き込む

LEDを装着できることを確認したら,まずはサンプルプログラムをネットから拾って動かしてみましょう.開発環境は「Digispark tutorial」で,プログラムは「Adafruit Neopixel library」でググれば出てきます(両方英語).書き込み方などの詳細は日本語で解説している個人ブログを参照してください. ただし,サンプルプログラムはそのままでは動きませんので手を加えます.例えば「buttoncycler」なら,次のようにマクロ定数を変えます.

#define BUTTON_PIN 21
#define PIXEL_PIN 60
#define PIXEL_COUNT 169

うまくいけば色々なパターンで光るLEDタワーが確認できるはずです.

www.youtube.com

7.プログラムを改造する

ここまで来たらあとは皆さんの思うがままにサンプルを改変しちゃいましょう.参考までに私はµ's仕様にしており,かよちん(Lime)→凛ちゃん(Yellow)→(・8・)(White)→ハノケチュン(Dark Orange)→まきちゃん(Red)→にこにー(Deep Pink)→のんちゃん(Dark Violet)→ンミチュン(Blue)→エリチカ(Cyan)→この順でレインボー,という感じにしました.()内の色名はweb用カラーコードを参考にしています.長押しでオフになり最初に戻ります.レインボーのみ長押し中は光が動きます.ソースコードは後で公開するかも.

www.youtube.com

終わりに

開発当初に思い描いていた,µ's9人を同時に応援できるペンライトを実現できて大変満足です.今後のオフライブも捗ると思います.今後の発展として,レイン棒を降った時に丁度光のメッセージが読めるような点滅パターンを仕込むなどすると更に表現の幅が広がるので,挑戦したいところです.

しかし残念なことに,多くのライブ(µ'sのライブも含む)では改造・自作ペンライトの使用が禁止されています.自作し終えてから気づいた私は今,ショックで軽く落ち込んでいます.レイン棒が公式に陽の目を見ることがあるのか非常に疑問ですが……

まぁ楽しいからいっか!

(追記:2017/02/04)要望があったのでソースコードを公開します.7.で作成したプログラムは以下の通りですが,その後ちょっと改変したので動画そのものの挙動ではありません.具体的には「終わりに」で述べた「レイン棒を降った時に丁度光のメッセージが読めるような点滅パターン」の実装を試みている部分があります(電源を入れた直後にピンクの点滅が起こりますが,暗い場所でいい感じに横に振るとNICOの文字が見えるかも?).あと,元のbuttoncyclerで使われていた関数は勉強のためそのまま残してあります.

// This is a demonstration on how to use an input device to trigger changes on your neo pixels.
// You should wire a momentary push button to connect from ground to a digital IO pin.  When you
// press the button it will change to a new pixel animation.  Note that you need to press the
// button once to start the first animation!

#include <Adafruit_NeoPixel.h>

#define BUTTON_PIN   1    // Digital IO pin connected to the button. 

#define PIXEL_PIN    0    // Digital IO pin connected to the NeoPixels.

#define PIXEL_COUNT  9    // Number of NeoPixels

#define SHOWTYPE_NUM  11   // Number of color patterns

//--------------------------------
//    μ's Color Code (R, G, B)
//--------------------------------
#define COLOR_NICO     0xff, 0x14, 0x93    //  Deep Pink
#define COLOR_MAKI     0xff, 0x00, 0x00    //  Red
#define COLOR_ELI      0x00, 0xff, 0xff    //  Cyan
#define COLOR_KOTORI   0xff, 0xff, 0xff    //  White Smoke
#define COLOR_HONOKA   0xff, 0x8c, 0x00    //  Dark Orange
#define COLOR_HANAYO   0x00, 0xff, 0x00    //  Lime
#define COLOR_RIN      0xff, 0xff, 0x00    //  Yellow
#define COLOR_UMI      0x00, 0x00, 0xff    //  Blue
#define COLOR_NOZOMI   0x94, 0x00, 0xd3    //  Dark Violet
#define COLOR_BLACK    0x00, 0x00, 0x00

const uint8_t colorCode[9][3] = {
                    {COLOR_HANAYO},
                    {COLOR_RIN},
                    {COLOR_KOTORI},
                    {COLOR_HONOKA},
                    {COLOR_MAKI},
                    {COLOR_NICO},
                    {COLOR_NOZOMI},
                    {COLOR_UMI},
                    {COLOR_ELI}};

const char str[4][5][9] = {
                    {{1,1,1,1,1,1,1,1,1},
                     {0,0,0,0,0,1,1,0,0},
                     {0,0,0,1,1,0,0,0,0},
                     {0,1,1,0,0,0,0,0,0},
                     {1,1,1,1,1,1,1,1,1}},      //  N
                    {{0,0,0,0,0,0,0,0,0},
                     {1,0,0,0,0,0,0,0,1},
                     {1,1,1,1,1,1,1,1,1},
                     {1,0,0,0,0,0,0,0,1},
                     {0,0,0,0,0,0,0,0,0}},      //  I
                    {{0,0,1,1,1,1,1,0,0},
                     {0,1,0,0,0,0,0,1,0},
                     {1,0,0,0,0,0,0,0,1},
                     {0,1,0,0,0,0,0,1,0},
                     {0,0,0,0,0,0,0,0,0}},      //  C
                    {{0,0,1,1,1,1,1,0,0},
                     {0,1,0,0,0,0,0,1,0},
                     {1,0,0,0,0,0,0,0,1},
                     {0,1,0,0,0,0,0,1,0},
                     {0,0,1,1,1,1,1,0,0}}};      //  O

// Parameter 1 = number of pixels in strip,  neopixel stick has 8
// Parameter 2 = pin number (most are valid)
// Parameter 3 = pixel type flags, add together as needed:
//   NEO_RGB     Pixels are wired for RGB bitstream
//   NEO_GRB     Pixels are wired for GRB bitstream, correct for neopixel stick
//   NEO_KHZ400  400 KHz bitstream (e.g. FLORA pixels)
//   NEO_KHZ800  800 KHz bitstream (e.g. High Density LED strip), correct for neopixel stick
Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);

bool oldState = HIGH;
int showType = 0;              // The Number of Color Patterns
unsigned long timeStamp = 0;

void setup() {
  pinMode(BUTTON_PIN, INPUT);
  strip.begin();
  colorWipe(strip.Color(COLOR_KOTORI), 30);
  delay(50);
  colorWipe(strip.Color(0, 0, 0), 30);
  delay(20);
  strip.show(); // Initialize all pixels to 'off'
}

void loop() {
  // Get current button state.
  bool newState = digitalRead(BUTTON_PIN);
  // Check if state changed from high to low (button press).
  if (newState == HIGH && oldState == LOW) {
    // Short delay to debounce button.
    delay(20);
    // Check if button is still low after debounce.
    newState = digitalRead(BUTTON_PIN);
    if (newState == HIGH) {
      showType++;
      if (showType > SHOWTYPE_NUM)
        showType = 0;
      startShow(showType);
      //  Set time stamp on pushing the button.
      timeStamp = millis();
    }
  }
  //  You can reset color pattern by pushing the button for 1 sec.
  else if(newState == HIGH && oldState == HIGH) {
    if((millis() - timeStamp) >= 1000) {
      showType = 0;
      startShow(showType);
    }
  }
  // Set the last button state to the old state.
  oldState = newState;
}

void startShow(int i) {
  switch(i){
    case 0: colorWipe(strip.Color(COLOR_BLACK), 40);    // Black/off
            break;
    case 1: colorCharactor(strip.Color(COLOR_NICO), str, 12);
            break;
    case 2: colorWipe(strip.Color(COLOR_HANAYO), 40);
            break;
    case 3: colorWipe(strip.Color(COLOR_RIN), 40);
            break;
    case 4: colorWipe(strip.Color(COLOR_KOTORI), 40);
            break;
    case 5: colorWipe(strip.Color(COLOR_HONOKA), 40);
            break;
    case 6: colorWipe(strip.Color(COLOR_MAKI), 40);
            break;
    case 7: colorWipe(strip.Color(COLOR_NICO), 40);
            break;
    case 8: colorWipe(strip.Color(COLOR_NOZOMI), 40);
            break;
    case 9: colorWipe(strip.Color(COLOR_UMI), 40);
            break;
    case 10: colorWipe(strip.Color(COLOR_ELI), 40);
            break;
    case 11: rainbowCycleForLoveLive(80);
            break;
  }
}

// Fill the dots one after the other with a color
void colorWipe(uint32_t c, uint8_t wait) {
  for(uint16_t i=0; i<strip.numPixels(); i++) {
      strip.setPixelColor(i, c);
      strip.show();
      delay(wait);
  }
}

void rainbowForLoveLive(uint8_t wait) {
  for(int i=0; i<9; i++) {
      strip.setPixelColor(i, strip.Color(colorCode[i][0],colorCode[i][1],colorCode[i][2]));
      strip.show();
      delay(wait);
  }
}

void colorCharactor(uint32_t c, const char str[][5][9], uint8_t wait) {
    do{
      for(int k=0; k<4; k++) {      //  num of charactors
        for(int j=0; j<5; j++) {    //  width of charactor
          for(int i=0; i<9; i++) {  //  height of charactor
            if(str[k][j][i])
              strip.setPixelColor(i, c);
            else
              strip.setPixelColor(i, strip.Color(COLOR_BLACK));
          }
          strip.show();
          delay(wait);
        }
      }
      for(int i=0; i<9; i++) {  //  height of charactor
        strip.setPixelColor(i, strip.Color(COLOR_BLACK));
      }
      strip.show();
      delay(wait*10);
  }while(digitalRead(BUTTON_PIN) == LOW);
}

void rainbowCycleForLoveLive(uint8_t wait){
  static int8_t j = 0;
  while(digitalRead(BUTTON_PIN) == HIGH) {
    for(int i=0; i<strip.numPixels(); i++) {
      strip.setPixelColor(i, strip.Color(colorCode[j][0], colorCode[j][1], colorCode[j][2]));
      j--;
      if(j<0) j=8;
    }
    strip.show();
    delay(wait);
    j++;
    if(j>8) j=0;
  }
}

void rainbow(uint8_t wait) {
  uint16_t i, j;

  for(j=0; j<256; j++) {
    for(i=0; i<strip.numPixels(); i++) {
      strip.setPixelColor(i, Wheel((i+j) & 255));
    }
    strip.show();
    delay(wait);
  }
}

// Slightly different, this makes the rainbow equally distributed throughout
void rainbowCycle(uint8_t wait) {
  uint16_t i, j;

  for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
    for(i=0; i< strip.numPixels(); i++) {
      strip.setPixelColor(i, Wheel(((i * 256 / strip.numPixels()) + j) & 255));
    }
    strip.show();
    delay(wait);
  }
}

//Theatre-style crawling lights.
void theaterChase(uint32_t c, uint8_t wait) {
  for (int j=0; j<10; j++) {  //do 10 cycles of chasing
    for (int q=0; q < 3; q++) {
      for (int i=0; i < strip.numPixels(); i=i+3) {
        strip.setPixelColor(i+q, c);    //turn every third pixel on
      }
      strip.show();
     
      delay(wait);
     
      for (int i=0; i < strip.numPixels(); i=i+3) {
        strip.setPixelColor(i+q, 0);        //turn every third pixel off
      }
    }
  }
}

//Theatre-style crawling lights with rainbow effect
void theaterChaseRainbow(uint8_t wait) {
  for (int j=0; j < 256; j++) {     // cycle all 256 colors in the wheel
    for (int q=0; q < 3; q++) {
        for (int i=0; i < strip.numPixels(); i=i+3) {
          strip.setPixelColor(i+q, Wheel( (i+j) % 255));    //turn every third pixel on
        }
        strip.show();
       
        delay(wait);
       
        for (int i=0; i < strip.numPixels(); i=i+3) {
          strip.setPixelColor(i+q, 0);        //turn every third pixel off
        }
    }
  }
}

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;
  if(WheelPos < 85) {
   return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  } else if(WheelPos < 170) {
    WheelPos -= 85;
   return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  } else {
   WheelPos -= 170;
   return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
  }
}

(2017/06/09追記) 要望があったので基板の写真を追加します。基本的に回路を銅線で繋いであるだけです。抵抗も特に入れていません。

PCLでOBJファイルを表示(点群のみ)

PCLのチュートリアルでは点群データのファイル拡張子として.pcdを使っていますが,.pcdでない既存のファイルからも点群データを読み込んで表示したいという需要は大いにあると思います.例えば.objとか.stlとか.成功したコードを2通り載せておきます.他にも見つかったら追記しようと思います.

なお,ここでは.objファイルとしてMMD標準モデルとして有名な「あにまさ式ミク」を利用します.元データは.pmdですが,.objファイルにしてくれた先人がいたので利用させてもらいます.感謝感激雨霰.

第一の方法:PolygonMesh→PointCloud

.objファイルはPCLで読み込むとpcl::PolygonMesh型となります.これは直接pcl::Visualization::CloudViewerで開くことができないので,pcl::PointCloud<T>型に変換してからCloudViewerに渡します.なおソースコード中のPCLAdapter.hについては以前の記事を参照のこと.

ちなみにpcl::io::loadPolygonFileOBJpcl::io::loadPolygonFileSTLに変更すれば.stlファイルも読み込めます.

#include "PCLAdapter.h"

const char filename[] = "model/miku.obj";

int main(void)
{
    // OBJファイルを読み込む
    pcl::PolygonMesh::Ptr mesh(new pcl::PolygonMesh());
    pcl::PointCloud<pcl::PointXYZ>::Ptr obj_pcd(new pcl::PointCloud<pcl::PointXYZ>());
    if (pcl::io::loadPolygonFileOBJ(filename, *mesh) != -1)
    {   // PolygonMesh -> PointCloud<PointXYZ>
        pcl::fromPCLPointCloud2(mesh->cloud, *obj_pcd);
    }
    while (!viewer.wasStopped())
    {
        viewer.showCloud(obj_pcd);
    }
}

第二の方法:PolygonMesh→vtkPolyData→PointCloud

なぜわざわざvtkPolyDataとかいう余計なものを間に挟むのか.まあいいじゃないか....っていう冗談はさておき.CloudViewerで物足りなくなった時に将来的にPCLVisualizerを使うことになると思います.こっちはvtkPolyDataを要求するので,この変換方法もメモとして残すためです.どうせGLFWに描かせるようにするからいらないかもしれないけど.

(2015/09/21訂正)間違い.PCLVisualizerはちゃんとPointCloudをそのまま読み込んでくれます.なのでこっちの方法はいよいよもって意味がなくなりました.

#include "PCLAdapter.h"

const char filename[] = "model/miku.obj";

int main(void)
{
    // OBJファイルを読み込む
    pcl::PolygonMesh::Ptr mesh(new pcl::PolygonMesh());
    pcl::PointCloud<pcl::PointXYZ>::Ptr obj_pcd(new pcl::PointCloud<pcl::PointXYZ>());
    if (pcl::io::loadPolygonFileOBJ(filename, *mesh) != -1)
    {   // PolygonMesh -> vtkPolyData -> PointCloud<PointXYZ>
        vtkSmartPointer<vtkPolyData> vtkmesh;
        pcl::io::mesh2vtk(*mesh, vtkmesh);
        pcl::io::vtkPolyDataToPointCloud(vtkmesh, *obj_pcd);
    }
    while (!viewer.wasStopped())
    {
        viewer.showCloud(obj_pcd);
    }
}

結果

f:id:Mzawa2:20150908230037p:plain ミクさんの神々しい御姿が点群データとして表示できました.ちなみに,CloudViewerは既にマウスドラッグ・キー入力操作が仕込まれていますので,OpenGLのように改めてプログラムする必要はありません.キー入力の一覧はhでコンソールにずらっと表示されます.qもしくはeを押すとviewer.wasStopped()がtrueを返し終了します.

こんな感じで今後もPCLのTipsを不定期にメモ書きしていく予定.

C++/CLIでOpenGL描画する方法

C++/CLIのフォーム画面にOpenGLレンダリングしたグラフィックを表示する方法をメモ書きします.なお,このTipsでは前回の内容をそのまま使っています.

C++/CLIOpenGLを描画する準備

1. OpenGLSimpleAdapter.hをインクルードする

nursの日記様が作成した便利なヘッダファイル,OpenGLSimpleAdapter.hを使う.ちなみに元ソースのstatic PIXELFORMATDESCRIPTOR pfdの引数の詳細のみここに載せておく.

    static PIXELFORMATDESCRIPTOR pfd = { 
            sizeof(PIXELFORMATDESCRIPTOR),   // size of this pfd 
            1,                     // version number 
            PFD_DRAW_TO_WINDOW |   // support window 
            PFD_SUPPORT_OPENGL |   // support OpenGL 
            PFD_DOUBLEBUFFER,      // double buffered 
            PFD_TYPE_RGBA,         // RGBA type 
            24,                    // 24-bit color depth 
            0, 0, 0, 0, 0, 0,      // color bits ignored 
            0,                     // no alpha buffer 
            0,                     // shift bit ignored 
            0,                     // no accumulation buffer 
            0, 0, 0, 0,            // accum bits ignored 
            32,                    // 32-bit z-buffer 
            0,                     // no stencil buffer 
            0,                     // no auxiliary buffer 
            PFD_MAIN_PLANE,        // main layer 
            0,                     // reserved 
            0, 0, 0                // layer masks ignored 
        }; 

2. MyFormのデザイナにpanelを追加する

OpenGLSimpleAdapter.hpanel要素にOpenGLの描画バッファを出力するようにしてある(pictureboxではないので注意).ツールボックス→コンテナー→Panel をフォームにドラッグして配置する.

f:id:Mzawa2:20150825023309p:plain

3. 追加したPanelにイベントを追加する

追加したPanelをクリックしてアクティブになっている状態で,プロパティ内のイベントPanelをダブルクリックする.この時MyForm.h内にイベントハンドラpanel1_Paint()が自動で追加される.

f:id:Mzawa2:20150825023318p:plain

MyForm.h内に次のインライン関数が追加される.

    private: System::Void panel1_Paint(System::Object^  sender, System::Windows::Forms::PaintEventArgs^  e){}

MyForm.hは多重インクルード禁止命令#pragma onceが書かれているのでここにコードを書き進めても良いのだが,自分の流儀だとMyForm.cppに分けたい.そこでMyForm.hのこいつを,

    private: System::Void panel1_Paint(System::Object^  sender, System::Windows::Forms::PaintEventArgs^  e);

と宣言に書き直し,MyForm.cpp

    #include "MyForm.h"
    using namespace testWindowsUI;  //  <-- ここにはプロジェクト名を入れる
    System::Void MyForm::panel1_Paint(System::Object^  sender, System::Windows::Forms::PaintEventArgs^  e)
    {
        //  ここに処理コードを書く
    }

と書いてやる.つまり,private:を消去し,関数名の前にMyForm::(WindowsUIの名前)を加えて,{}を入れてそこにコードを書くという流れ.今後関数を追加するたびにこの様にMyForm.hからMyForm.cppに移す必要がある(プロジェクト名のnamespaceは最初のみ追加する).

これと同様に,フォーム画面全体をアクティブにした状態で,プロパティからMyForm::MyForm_Load()関数も追加してやる.

4. MyForm.h内に次のコードを追加する

    #pragma once
    #include "OpenGLSimpleAdapter.h"  // ⇐ ここ!
    namespace testWindowsUI {
    //----------中略------------- 
   #pragma endregion
        public: OpenGLSimpleAdapter^ GLAdapter;  // ⇐ ここ!
    private: System::Void panel1_Paint(System::Object^  sender, System::Windows::Forms::PaintEventArgs^  e); 
        private: System::Void MyForm_Load(System::Object^  sender, System::EventArgs^  e);
    };
}

ついでにMyForm.cpp内もコードを追加.

    System::Void MyForm::MyForm_Load(System::Object^  sender, System::EventArgs^  e)
    {
        GLAdapter = gcnew OpenGLSimpleAdapter(GetDC((HWND)panel1->Handle.ToPointer()));
    }

    System::Void MyForm::panel1_Paint(System::Object^  sender, System::Windows::Forms::PaintEventArgs^  e) 
    {
        GLAdapter->BeginRender();
        {
        }
        GLAdapter->EndRender();
    }

これでやっとOpenGLが描けるようになった.BeginRender()EndRender()の間の{}内に,自由にOpenGLのコードを書くことができる(実は{}はいらないんだけど可読性がよくなるのでつけてる).ちなみにEndRender()glutSwapBuffers()と同じ機能を有する.例えば次のように直線を書いてみる.

    GLAdapter->BeginRender();
    {
        glClearColor(0, 0, 0, 0);
        glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
        glOrtho(0, panel1->Width, panel1->Height, 0, -1, 1);
        glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
        glLineWidth(1.0);
        glBegin(GL_LINES);
        glVertex2i(20, 40);
        glVertex2i(200, 180);
        glEnd();
    }
    GLAdapter->EndRender();

描画結果は次の通り.

f:id:Mzawa2:20150825023353p:plain

UIから動的にOpenGLをいじってみる

スクロールバーを使ってアニメーションを作ってみる.

1. スクロールバー,ラベルを追加

f:id:Mzawa2:20150825023359p:plain:w320:h240

2. ValueChangedイベントハンドラを追加

いつもの様にhScrollBar1_ValueChanged()MyForm.hからMyForm.cppに移す.

f:id:Mzawa2:20150825023407p:plain

3. MyFormクラスの中にpanelRefresh()関数を追加

この関数にはGLUTでいうglutDisplayFunc()に登録すべき描画コールバック関数の役割を持たせる.そして次の様にコードを書く.

MyForm.h
    #pragma once
    //---------中略------------
    #pragma endregion
            public:
                OpenGLSimpleAdapter^ GLAdapter;
                System::Void panelRefresh(void); // ⇐ ここ
        private: System::Void panel1_Paint(System::Object^  sender, System::Windows::Forms::PaintEventArgs^  e);
        private: System::Void MyForm_Load(System::Object^  sender, System::EventArgs^  e);

        private: System::Void hScrollBar1_ValueChanged(System::Object^  sender, System::EventArgs^  e);
        };
    }
MyForm.cpp
    #include "MyForm.h"
    #include <math.h>
    using namespace testWindowsUI;
    static double th = 0.0;
    System::Void MyForm::MyForm_Load(System::Object^  sender, System::EventArgs^  e)
    {
        GLAdapter = gcnew OpenGLSimpleAdapter(GetDC((HWND)panel1->Handle.ToPointer()));
    }
    System::Void MyForm::panel1_Paint(System::Object^  sender, System::Windows::Forms::PaintEventArgs^  e) 
    {
        panelRefresh();
    }
    System::Void MyForm::hScrollBar1_ValueChanged(System::Object^  sender, System::EventArgs^  e)
    {
        th = (double)hScrollBar1->Value / (double)hScrollBar1->Maximum * 8.0 * atan(1.0);
        label1->Text = th.ToString("F2");
        panelRefresh();
    }
    System::Void MyForm::panelRefresh(void)
    {
        GLAdapter->BeginRender();
        {
            // Initialize
            glClearColor(0, 0, 0, 0);
            glMatrixMode(GL_PROJECTION);
            glLoadIdentity();
            glOrtho(-1, 1, -1, 1, -1, 1);
            // Drawing
            glClear(GL_COLOR_BUFFER_BIT);
            glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
            glLineWidth(1.0);
            glBegin(GL_LINES);
            glVertex2d(0, 0);
            glVertex2d(cos(th), sin(th));
            glEnd();
        }
        GLAdapter->EndRender();
    }
実行結果

f:id:Mzawa2:20150825023412p:plain

スクロールバーを動かすとリアルタイムに直線の傾きが変わる.

C++/CLIでウィンドウを生成する方法(VS2013)

すでにネットの海に腐るほどあるTipsですが,自分用メモとして残します.画像サイズがまちまちなのはWordファイルからの移植だからだと思います.

ちなみに開発環境はVisual Studio 2013 Expressです.

 

1. 「新しいプロジェクト」で「空のプロジェクト」を選択する

なぜかって?stdfx.hがうざったくてしょうがないからさ!

f:id:Mzawa2:20150822140830p:plain:w320:h240

ちなみに間違って「Win32 プロジェクト」を選択してしまった時は,この後に出てくるウィザードで即完了を押さずに「追加のオプション」の「空のプロジェクト」オプションを追加すれば同じ状態になる.

2. 「ソースファイル」に「追加→新しい項目」で「Windows フォーム」を追加する

Visual C++ → UIの中に入ってる.

f:id:Mzawa2:20150822140843p:plain:w320:h240

Windowsフォームを追加するとCLRに変換しますうんたらかんたらと忠告が出る.OKを押してやると画像のようになる.このままでは起動できないので,ソースコードを追加したりプロパティに若干の変更を加えなくてはならない.

f:id:Mzawa2:20150822140849p:plain:w320:h240

3. フォーム起動用のメインソースを追加する

ここでは名前をmain.cppとした.

4. 次のソースコードをmain.cppに書く

#include "MyForm.h" //⇐ フォームデザインの名前
using namespace testWindowsUI;  //⇐ プロジェクト名
[STAThreadAttribute]
int main(array<System::String ^> ^args)
{
    Application::EnableVisualStyles();
    Application::SetCompatibleTextRenderingDefault(false);
   Application::Run(gcnew MyForm()); //⇐ フォームデザインの名前
   return 0;
}

この状態だとなんか色々読み込まれてないので,これからプロパティを設定する.

5. プロパティを編集する

「プロジェクト」を右クリックするとプロパティウィンドウが開くので色々修正していく. デフォルトでは「共通言語ランタイムサポートを使用しない」になっているはずなので,まずは「全般→共通言語ランタイムサポート」を「共通言語ランタイムサポート(/clr)」に変更する. ※DebugとReleaseの両方でやっておくと後々便利(左上の「構成(C)」で変更可能).迷ったら「すべての構成」にしておこう.

f:id:Mzawa2:20150822140903p:plain:w320:h240

6. ビルドを確認する

この状態で一応ビルドして起動できる.成功すると MyForm という空のウィンドウと,その裏にコンソール画面が開いている状態になる.

f:id:Mzawa2:20150822140908p:plain:w320:h240

7. コンソール画面を消す

printf()デバッグができるので便利なコンソール画面.「やっぱ邪魔だよこれ!」ってなったらプロジェクトのプロパティから修正できる. リンカー→システム→サブシステム をWindows(/SUBSYSTEM:WINDOWS)に変更し, リンカー→詳細設定→エントリポイント にmainと書き込む.これが最初に呼び出される関数である.

f:id:Mzawa2:20150822140913p:plain:w320:h240 f:id:Mzawa2:20150822140917p:plain:w320:h240

この状態でビルドすると,ちゃんと後ろのコンソールが消えてくれる.

f:id:Mzawa2:20150822140924p:plain:w320:h240

PCLのリンカスクリプト"PCLAdapter.h"


コーディング関連の記事を書くためのブログなのに載せるの2回目という...

最近になってPCL(Point Cloud Library)の勉強を始めました.日本語資料が少なくて大変なこのライブラリ,まともにプロジェクトに入れようとするとlibファイル多すぎてやってらんないので,みんなで使える便利なリンカスクリプトを作ってみました.

ちなみに自分のビルド環境は,

です.特にPCLが他のバージョンだとlibファイルのディレクトリや名前が変わってたりするので注意. x64の場合は環境変数を変えれば動くと思う.

(2015/09/08追記)下のソースコードの内容を変更しました.

  • リンクし忘れていたライブラリを追加
  • #pragma regionによるコードの簡素化
  • C4996対策の変更

使い方

  1. 以下のコードをコピーして.hで保存する.
  2. 環境変数PCL_INCPCL_LIBを追加し,コメントに記載された値をコピペする.
  3. VisualStudioを起動している場合はVSを一旦再起動する.
  4. プロジェクトのプロパティで,「追加のインクルードディレクトリ」に&(PCL_INC)を,「追加のライブラリディレクトリ」に&(PCL_LIB)をそれぞれ追加する.
  5. PCLを使いたいコードにインクルードする.
  6. PCLAdapter.h 内の#includeは必要に応じて追加,削減する.

ここではすべてのライブラリをリンクしていますが,プログラムを軽くしたいなどの事情がある場合は不要なライブラリをコメントアウトしましょう.ただし,どうやらboostは必須で,特徴点検出をしたい場合はflannを入れないと動かないようです.

PCLを使う上での注意点

自分ではまってしまった箇所のメモ書き.ソースコードはこの下です.

「flannがあいまいです」

OpenCVを使っている場合,

using namespace cv;

を使うコーディングだと,こんな風に怒られてコンパイルが通らないことがあります.これはエラー文の通り,cv::がないせいでPCLのサードパーティ内のflann::OpenCVcv::flann::が競合してしまうためです.これを解決するには,PCLのflannを使う可能性のある場所でusing namespace cv;が効かないようにすると良いです.例えばOpenCVのみを使う.cpp内でのみ名前空間の省略を効かせるようにするとか.

ライブラリ内に潜む罠

自分が使ったPCLのバージョンでは,幾つかのライブラリでfopen()が使われているせいで,セキュリティにうるさいVC++コンパイラちゃんはここぞとばかりにfopen_s()を推しながら

えーマジー?fopen()ー!?

キモーい!

fopen()が許されるのはVC2006までだよねー

キャハハハハハ!

と罵ってきます(嘘).こいつらを黙らせるにはライブラリを書き換える以外にも,プロジェクトのプロパティで「C/C++ -> 詳細設定 -> 指定の警告を無視する」に4996を入れてerror:C4996を無視させるやり方があります.一応大丈夫なはず.

(2015/09/08追記)もっといいやり方が見つかりました.VC++コンパイラにセキュリティ対策を自動でやってもらう方法です.(引用元: ☆PROJECT ASURA☆ [OpenGL] 『メッシュを読み込む!!(1) 〜OBJファイル〜』

#pragma region Disable Waring C4996
//
// Disable Warning C4996
//
#ifndef _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES
#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1
#endif
#ifndef _CRT_SECURE_CPP_OVERLOAD_SECURE_NAMES
#define _CRT_SECURE_CPP_OVERLOAD_SECURE_NAMES 1
#endif
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#pragma endregion

このようにすると,fopen()fopen_s()に自動で書き直されるみたいです.めっちゃ便利.

PCLAdapter.h

//------------------------------------------------------------
//     Point Cloud Library Adapter (Library Linker)
//------------------------------------------------------------

/*
   pclを利用するためのリンカスクリプト
   includeは必要なものを書き足していく

   環境変数設定 (PCL v1.7.2 32bit vc120)

   $(PCL_INC) =
       C:\Program Files (x86)\PCL 1.7.2\include\pcl-1.7;
       C:\Program Files (x86)\PCL 1.7.2\3rdParty\Boost\include\boost-1_57;
       C:\Program Files (x86)\PCL 1.7.2\3rdParty\Eigen\eigen3;
       C:\Program Files (x86)\PCL 1.7.2\3rdParty\FLANN\include;
       C:\Program Files (x86)\PCL 1.7.2\3rdParty\Qhull\include\libqhullcpp;
       C:\Program Files (x86)\PCL 1.7.2\3rdParty\VTK\include\vtk-6.2;

   $(PCL_LIB) = 
       C:\Program Files (x86)\PCL 1.7.2\lib;
       C:\Program Files (x86)\PCL 1.7.2\3rdParty\Boost\lib;
       C:\Program Files (x86)\PCL 1.7.2\3rdParty\FLANN\lib;
       C:\Program Files (x86)\PCL 1.7.2\3rdParty\Qhull\lib;
       C:\Program Files (x86)\PCL 1.7.2\3rdParty\VTK\lib;

*/


#ifndef PCLADAPTER_H_
#define PCLADAPTER_H_

#pragma region Disable Waring C4996
//
// Disable Warning C4996
//
#ifndef _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES
#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1
#endif
#ifndef _CRT_SECURE_CPP_OVERLOAD_SECURE_NAMES
#define _CRT_SECURE_CPP_OVERLOAD_SECURE_NAMES 1
#endif
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif
#pragma endregion

// インクルード
#pragma region Includes
#include <pcl\point_types.h>
#include <pcl\common\io.h>
#include <pcl\io\pcd_io.h>
#include <pcl\io\vtk_lib_io.h>
#include <pcl\visualization\cloud_viewer.h>          // PCLVisualizer
#include <pcl\keypoints\harris_3d.h>             // Harris特徴検出器
#include <pcl\features\fpfh.h>
#include <pcl\features\normal_3d.h>
#include <pcl/segmentation/sac_segmentation.h>       // 平面検出
#pragma endregion
// ビルドモード
#pragma region Build Mode
#ifdef _DEBUG
#define PCL_EXT_STR "_debug.lib"
#define PCL_BOOST_EXT_STR "-vc120-mt-gd-1_57.lib"
#define PCL_FLANN_EXT_STR "-gd.lib"
#define PCL_QHULL_EXT_STR "_d.lib"
#define PCL_VTK_EXT_STR "-6.2-gd.lib"
#else
#define PCL_EXT_STR "_release.lib"
#define PCL_BOOST_EXT_STR "-vc120-mt-1_57.lib"
#define PCL_FLANN_EXT_STR ".lib"
#define PCL_QHULL_EXT_STR ".lib"
#define PCL_VTK_EXT_STR "-6.2.lib"
#endif
#pragma endregion
// ライブラリのリンク(不要な物はコメントアウト)
#pragma region PCL Linker Script
#pragma comment(lib, "pcl_common"           PCL_EXT_STR)
#pragma comment(lib, "pcl_features"         PCL_EXT_STR)
#pragma comment(lib, "pcl_filters"          PCL_EXT_STR)
#pragma comment(lib, "pcl_io"               PCL_EXT_STR)
#pragma comment(lib, "pcl_io_ply"           PCL_EXT_STR)
#pragma comment(lib, "pcl_kdtree"           PCL_EXT_STR)
#pragma comment(lib, "pcl_keypoints"        PCL_EXT_STR)
#pragma comment(lib, "pcl_octree"           PCL_EXT_STR)
#pragma comment(lib, "pcl_outofcore"        PCL_EXT_STR)
#pragma comment(lib, "pcl_people"           PCL_EXT_STR)
#pragma comment(lib, "pcl_recognition"      PCL_EXT_STR)
#pragma comment(lib, "pcl_registration"     PCL_EXT_STR)
#pragma comment(lib, "pcl_sample_consensus" PCL_EXT_STR)
#pragma comment(lib, "pcl_search"           PCL_EXT_STR)
#pragma comment(lib, "pcl_segmentation"     PCL_EXT_STR)
#pragma comment(lib, "pcl_surface"          PCL_EXT_STR)
#pragma comment(lib, "pcl_tracking"         PCL_EXT_STR)
#pragma comment(lib, "pcl_visualization"    PCL_EXT_STR)
#pragma endregion
#pragma region LibBoost Linker Script
#pragma comment(lib, "libboost_atomic"      PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_chrono"      PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_container"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_context"     PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_coroutine"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_date_time"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_exception"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_filesystem"      PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_graph"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_iostreams"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_locale"      PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_log_setup"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_log"     PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_math_c99f"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_math_c99l"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_math_c99"        PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_math_tr1f"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_math_tr1l"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_math_tr1"        PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_mpi"     PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_prg_exec_monitor"        PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_program_options"     PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_random"      PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_regex"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_serialization"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_signals"     PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_system"      PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_test_exec_monitor"       PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_thread"      PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_unit_test_framework"     PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_wave"        PCL_BOOST_EXT_STR)
#pragma comment(lib, "libboost_wserialization"      PCL_BOOST_EXT_STR)
#pragma endregion
#pragma region FLANN Linker Script
#pragma comment(lib, "flann"        PCL_FLANN_EXT_STR)
#pragma comment(lib, "flann_s"      PCL_FLANN_EXT_STR)
#pragma comment(lib, "flann_cpp_s"  PCL_FLANN_EXT_STR)
#pragma endregion
#pragma region QHull Linker Script
#pragma comment(lib, "qhull"    PCL_QHULL_EXT_STR)
#pragma comment(lib, "qhull_p"  PCL_QHULL_EXT_STR)
#pragma comment(lib, "qhullcpp" PCL_QHULL_EXT_STR)
#pragma comment(lib, "qhullstatic"  PCL_QHULL_EXT_STR)
#pragma comment(lib, "qhullstatic_p"    PCL_QHULL_EXT_STR)
#pragma endregion
#pragma region VTK Linker Script
#pragma comment(lib, "vtkalglib"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkChartsCore"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkCommonColor"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkCommonComputationalGeometry"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkCommonCore"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkCommonDataModel"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkCommonExecutionModel"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkCommonMath"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkCommonMisc"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkCommonSystem"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkCommonTransforms"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkDICOMParser"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkDomainsChemistry"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkexoIIc"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkexpat" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersAMR"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersCore"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersExtraction" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersFlowPaths"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersGeneral"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersGeneric"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersGeometry"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersHybrid" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersHyperTree"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersImaging"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersModeling"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersParallel"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersParallelImaging"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersProgrammable"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersSelection"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersSMP"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersSources"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersStatistics" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersTexture"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkFiltersVerdict"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkfreetype"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkftgl"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkGeovisCore"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkgl2ps" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkhdf5_hl"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkhdf5"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkImagingColor"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkImagingCore"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkImagingFourier"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkImagingGeneral"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkImagingHybrid" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkImagingMath"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkImagingMorphological"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkImagingSources"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkImagingStatistics" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkImagingStencil"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkInfovisCore"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkInfovisLayout" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkInteractionImage"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkInteractionStyle"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkInteractionWidgets"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOAMR" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOCore"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOEnSight" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOExodus"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOExport"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOGeometry"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOImage"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOImport"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOInfovis" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOLegacy"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOLSDyna"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOMinc"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOMovie"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIONetCDF"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOParallel"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOParallelXML" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOPLY" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOSQL" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOVideo"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOXML" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkIOXMLParser"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkjpeg"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkjsoncpp"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtklibxml2"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkmetaio"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkNetCDF_cxx"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkNetCDF"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkoggtheora" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkParallelCore"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkpng"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkproj4" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingAnnotation"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingContext2D"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingContextOpenGL"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingCore" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingFreeType" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingFreeTypeOpenGL"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingGL2PS"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingImage"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingLabel"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingLIC"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingLOD"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingOpenGL"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingVolume"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkRenderingVolumeOpenGL" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtksqlite"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtksys"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtktiff"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkverdict"   PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkViewsContext2D"    PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkViewsCore" PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkViewsInfovis"  PCL_VTK_EXT_STR)
#pragma comment(lib, "vtkzlib"  PCL_VTK_EXT_STR)
#pragma endregion 

#endif // PCLADAPTER_H_

ソリッドレイ研究所30周年記念イベント「EVA」感想

就活中でメンタルがヤバいのでずっと放置してたブログですが,落ち着いてきたのでちょっと近況報告.

6/4, 5に開催されたVRソリューションを扱う会社「ソリッドレイ研究所」の一大イベント「EVA~Exciting Visual Academy~」に参加してきました.このイベント,展示も講演も非常に面白くて,思わず詳細なレポートを書かざるを得ないくらい興奮してしまいました.というわけで,聞いた講演の部分の感想をまずはアップしたいと思います.特に講演2の方を詳細に.

著作権の関係上,講演の要約・写真等は掲載しませんのでご容赦を.

特別講演1「クロスモーダル表現とエンタテイメント」

講師:河合 隆史 氏(早稲田大学 基幹理工学部 表現工学科 教授)

講演内容:クロスモーダルとは、特定の感覚情報から他の感覚情報を補完して理解・体験する、人間の認知特性を活用したメディア表現の総称です。本講演では、クロスモーダル表現の応用事例やエンタテイメント分野における展望などを紹介していただきます。

 3D表現,人間心理,クロスモーダルの分野で有名な先生です.本講演では,3Dコンテンツを制作する際の定量的指針として,コンテンツを受け取る側の人間の心理特性(言語的反応,生理的変動)を考慮すべきという考えが流れていました.このように,人とテクノロジーを結び付けたり,人間の認知を分析したりする分野では心理物理学技法がよく使われています.これは一言でいえば「人間を,物理量を入力とし心理応答を出力とする一種のブラックボックスとみなし,それらの関係式から人間というシステムの性質を探る」やり方です.何しろ実験を工夫しさえすれば,解剖学的に調べずとも複雑かつ定量的な知見が得られるので,非常に強力なアプローチです.もちろん生理的な変動を測定しなければ片手落ちですが.

心理学を応用するのは何も精神科医やマジシャン,詐欺師ばかりではなく,人間工学的にも大変重要です.実際,実験的に河合先生の成果がコンテンツに応用されていたというのは凄く面白かったです.認知科学の応用で,人間の情動でさえも狙って定量的に設計できる可能性を見事に示してくれました.今後のコンテンツ制作やUI設計開発などの現場で,この手法はますます流行っていくことでしょう.

さてクロスモーダルについてですが,実は自分の研究室では散々聞いたような内容だったのですが知らない人にとってこんなに驚くべき話はないと思います.視覚が嗅覚や触覚の程度を変えるのみならず,実際に与えている感覚と異なった感覚になってしまい,更には与えていない刺激まで感じるようになる.このように錯覚現象を引き起こすため,うまく使えばVR・AR技術の発展にも大きく貢献します.様々な応用例を見せられて,この分野の研究が大きな可能性を持つことを改めて思い知りました.ただ錯覚を調べて喜んでるだけじゃないのね.研究がんばろっと.

特別講演2「VRでアイドルを誕生させる可能性」

講師:原田 勝弘 氏 / 玉置 絢 氏 (株式会社バンダイナムコエンターテインメント)
講演内容:鉄拳プロジェクトが独自に3年間VR技術を研究し、そのノウハウを結集したことで話題を呼んだ「サマーレッスン」を例に、VRにおけるキャラクター存在感など、ビジネスになりえるVRコンテンツの可能性をご講演していただきます。

実は今回の主目的はこの講演です.バンナムのVRアイドルというから「○イドルマ○ター」の話も出るかと思ったら殆ど触れられず,アイドルの話でもなかったから出鼻をくじかれましたが,良い意味で期待を裏切られました.VRで人間を表現する際に気を付けるべきことについて,「サマーレッスン」の開発が実に多くの示唆を与えており,大変興味深い内容でした.サマーレッスン自体はこの講演で初めて知りましたが,なるほどこりゃ物議を醸しだすわけだ.VRでもないムービートレーラーを見るだけで近すぎてドキドキしちゃうもんw

様々なエピソードがありましたが,「本当に生身の女の子を観察したことがあるのか!」という叱責が特に印象的で,没入間の高いHMDでのVRキャラクターだと可愛さの基準が現実と同じところに向かうということに驚きました.これは今後のインタラクティブなゲーム開発で非常に重要な意味を持っていると思います.なぜなら,開発者が「現実の女の子の可愛さ」を詳細に知っている必要があるということは,今までの観賞を主とするオタクコンテンツ的な文脈での「可愛さ」から脱却しなくてはならないからです.つまり,これまで以上に開発チームに違和感の元基準となる「現実的でまともな感性」が要求されることになり,要はリア充が開発チームにも求められることになるのです.ますます非リアのオタクの居場所が...そうするとインタラクティブなVRが提供するコンテンツは,アニメに代表される現在のオタクコンテンツとは別物になっていくのかもしれませんね.

リアルなゲーム特有のいわゆる「不気味の谷」というワードこそ使っていませんでしたが,今回の講演内容はまさにそれとの戦いだったように思います.全講演を通して不気味の谷の発生原因と乗り越え方をケーススタディでレクチャーしながら,その境目を分析し「VRキャラクターに主観を感じるかどうか」と一言で表現しきったことの意味は大きいと思います.今回の処方箋はあくまで一例ですが,現在の技術でもはや不気味の谷を越える技術的準備はできていたこと,我々が知るべきはむしろVRキャラクターの設計手順の方であったことを示したことで,不気味の谷を越えた先にある輝かしいVRコンテンツ群に繋がる橋が見えた気がしました.ただ2次元キャラはそのままレンダリングすると普段見るより異様に映ってしまうとのことで,更なるブレイクスルーが待たれるとのこと.でもまあプリ○ュアを始めとする各種女児アニメやラブラ○ブなどを擁するサンライズのトゥーンレンダリング技術をもってすれば数年と待たずに出来そうな気もするけど.ぜひ頑張ってください.

VRが自分の過去の体験と密接に絡み合うこと,化粧の例に代表されるようにVRキャラクターの可愛さの要因が現実のそれと同じであることを考慮すると,VRは決して虚構的な仮想体験のみにあらず,現実の一部なのだと強く思い,まさに本来の英語の意味でのVirtualが言葉として適切なのだと思い知りました.

バーチャル (virtual) とは,The American Heritage Dictionary によれば,「Existing in essence or effect though not in actual fact or form」と定義されている.つまり,「みかけや形は原物そのものではないが,本質的あるいは効果としては現実であり原物であること」であり,これはその ままバーチャルリアリティの定義を与える.(日本VR学会HP)

人間は世界の物理的現象をそのまま認識することはできず,必ず五感を通してしか世界を認識できません.五感というフィルターを通した世界の知覚こそがその人にとっての「世界」であるならば,VR技術で得られる体験もまた立派な「世界の一部」です.であれば猶更,VRという文字通り世界を作り替える技術との適切な付き合い方を早急に考える必要がある気がしてきました.これは決して規制とかそういうのじゃなくて,正しく知識を蓄えて来るべき日に備えよう,ということです.あらゆるテクノロジーは包丁と同じで道具に過ぎません.進化したテクノロジーを正しく使えるように人も進化しなくてはならないのではないでしょうか

まとめ

分量から察せる通り,自分にとって2番目の講演のインパクトが大変なものでした.もちろん1番目も非常に興味深い内容だったんですけれど,新しい知見という点でどうしても後者に軍配が上がってしまいました.今でも興奮覚め止まないのでここに書いてない内容でも色々語りたいことがあるんですが,とりあえずこの辺でやめときます.

スクフェスで学ぶ確率・統計(検定編)

前回の記事では,スクフェスを題材にしてベイズ的アプローチで確率推定してみました.今回はデータ収集・推定した結果をもとに細かな検証をしてみたいと思います.実は前回ベイズ推定で事後確率分布まで求めているので,それを使って信頼区間を示せば検定なんてしなくてもいいんだけど,今回は勉強のためやってみます.

 

例題

メドフェスでは金報酬,銀報酬の出現確率を上昇させるアイテム「金銀報酬UP(以下金銀UP)」がある.このアイテムにどれほどの効果があるか確認せよ.なお,度数分布表は次の通りである.ちなみに,本データはラブカストーンを1個も使わずになるべくLPを漏らさないようにして計測した.

 

合計

金銀UPなし

4

38

90

132

金銀UPあり

4

49

73

126

理論

例題を噛み砕くと,

金銀UPを付けることによって確率が本当に変化するのか,変化するとしたらどれだけ変化するのか

という問いになる.即ち,金銀UPの有無による確率変動有意があるかどうかを調べる必要がある.最近ではこの種の検定を廃止しようとする動きがあるみたいだが,正しく理解して気をつけているなら問題無いはずだ(この議論の背景についてはおまけ参照).

 

統計学における検定(統計学的仮説検定)は,以下の4ステップで行われる.

  1. 本当に示したい事とは逆の仮説を帰無仮説として立てる.
  2. 有意水準を決定する.(大抵0.05,より厳しく見る場合は0.01)
  3. データを収集する.
  4. データと帰無仮説からp値を計算する.
  5. p値に基づいて帰無仮説を評価し,有意水準より小さければ帰無仮説を棄却する.

ここでp値とは,

帰無仮説が正しい場合に,観測データと同等,またはそれよりも極端なデータが得られる確率

である.つまり,p値が小さければ小さいほど,帰無仮説が正しいとする根拠が薄れるというわけだ.このように統計学的仮説検定は一種の背理法のような論法で進めていく.

やってみた

今回のケースでは1個のサンプルにつき金,銀,銅の3通りが考えられるので,「はい/いいえ」のようにカテゴリが2通りの際に使われる検定法は使えない.かといってより一般的なカイ2乗検定を使おうにも,金銀UPの有無によらずサンプル数に対して金報酬の出た数が少なすぎて使えない.試しに期待値を計算すると次の表の通りになる.計算式は,行合計×列合計÷全体合計である.

 

金銀UPなし

4.09

44.5

83.4

金銀UPあり

3.91

42.5

79.6

一般に,表中に期待値が5以下のマスが全体の25%以上ある場合は不適だといわれている(これをCochran’s ruleという).統計にあまり詳しくない私はここで困ってしまったが,この場合はどうやらFisherの正確検定を使えばよいとのこと.2x2でない場合のFisherの正確検定に関しては文献を探しても数式が載ってなかったので宿題としておいて,計算できるサイトがあったのでこちらで計算してみる.

http://aoki2.si.gunma-u.ac.jp/exact/fisher/fisher.cgi

 

結果,p=0.202であるため,有意水準としてよく使われる0.05を大幅に超えている.従って結論として,今回のデータからは金銀UPの有無による有意差は認められず,帰無仮説を棄却できない.つまり,「確定的に金銀UPに差があるとは言えない」ということになる.

ただし注意して欲しいのは,この結果は「金銀UPに意味はない」とは一言も言ってない(原理的に有意差検定で帰無仮説を積極的に肯定することはできない).検出力が低く,この程度のサンプル数では有意差を検出することができないという可能性がある.

従って「実際には差があるのにないと判定された(第2種の過誤)」可能性もあるため,一応,効果量も計算してみる(ちゃんと調べてないのでこのあたりの計算は怪しいです.間違ってたら指摘をお願いします).公式は次の通り.

f:id:Mzawa2:20150323070957p:plain

結果,d_g=0.10, d_s=2.38, d_c=2.37 であった.一般的にd>0.8なら効果大だと判断できるので,銀報酬と銅報酬に関しては相当な効果があったといえる.

 

ちなみにベイズ推定の結果,両者のDirichlet分布は次のようになった.比較しやすいように目盛りは合わせてある.

f:id:Mzawa2:20150323070225p:plain

f:id:Mzawa2:20150323070216p:plain

上が金銀UPなし,下が金銀UPありの場合の確率分布である.座標が報酬の確率,標高がその座標(確率)の尤もらしさを与える.有意差が検出されなかったとはいえ,確率分布の山はかなり違う場所にできている(これが効果量の大きさに反映されている).おおよそだが,銀報酬の確率は30%から40%に増加しているのではないかと推測できる.金報酬については殆ど違いがわからない.

結論

統計的に有意とは言えなかったが,金銀UPを導入することにより,銀報酬の確率に大きな効果があった.約30%から40%に上昇するものと思われる.

一方金報酬については,サンプル数が少なすぎるせいか,有意差も効果も見られなかった.逆に言えば,ラブカストーンを使わずにメドフェスをプレイするならば,金銀UPの効果は誤差の範囲内であろう.つまり,イベントを走る気がないならば,金報酬UPを目的にして金銀UPを使用するのは得策ではない.

 

…という結論になりますが,本当に我々が欲しいのは金報酬ではなく勧誘チケットです.勧誘チケットはごく低い確率で銀報酬からも出るらしいので,もしかしたら金銀UPに意味があるかもしれません.ただ全87回銀報酬が出て未だ一度も勧誘チケットが来てないので,ベイズ的に1/87以下であると推定できます(おそらく実質1%くらい?).この結果をどう捉えるかは皆さん次第です.

(2015/9/7追記:定量的な統計はとっていませんが、通算100回程度銀報酬を引いたところでやっと銀報酬から勧誘チケットが出てきました。実質1%程度というのは正しそうです。)

ただ,金銀UPは10万Gと高額に見えますが,これをつけることにより銀報酬の5万Gが出やすくなるので,実質もうちょっと低いです.Gが余ってる人は戯れにやってみると良いかもしれませんね.

 

 - ちなみに今回の検証では,金銀UPをつけた場合は勧誘チケット2枚,つけなかった場合は1枚も来ませんでした

 

おまけ:統計学的検定を行う際の注意点

ここは自分用メモなので読み飛ばしても一向に構いません.

 

統計学的仮説検定(有意差検定)は,心理学や医学,その他多くの分野の論文で昔から用いられている.しかし実に多くの人が誤って理解し誤った使い方をしているせいで従来から批判が多い.では何が問題なのだろうか.

 

第1の問題は,有意差検定で最も重要なp値がサンプル数にも依存するということである.実はサンプル数を増やせばp値をいくらでも小さくできる.従って,たとえば悪意ある医学研究者Oは次のようなことができる.

1)Oは開発した新薬によって細胞を変質することに成功したと論文で発表したい.そこで新薬を滴下した細胞と滴下しない細胞を比較することにしたが,有意差検定の結果p=0.07と帰無仮説を否定できない非常に微妙な値が出てしまった.困ったOはサンプル数を増やしてもう一度実験を行なったところ,p<0.05になったので安心し,新薬に効果ありと発表した.

2)Oの新薬を滴下した細胞と水を滴下した細胞とを比較したところ,予想外に水の細胞が変質してしまった.慌てて有意差検定を行なったら,水の有意差はp=0.10であった.今のところ有意水準を上回っているので,Oは他に理由をつけてサンプル数をこれで適切ということにした.

当然,こんな態度の論文にケツを拭く紙以上の価値はない.ちなみに1)でサンプル数を変更しなかった場合でもたまーにp値が有意水準を下回る時がある.その一例だけをもってきて効果ありとするのは,再現性を欠くという意味で全く科学的ではない(ちなみにこれが第1種の過誤の具体例である).

このp値とサンプル数との問題を回避するために,効果量をp値と共に示す,95%信頼区間を示す(信頼区間から外れていれば有意とすぐわかる),事前に検出力を計算して適切なサンプル数を見積もる,などの対策が挙げられる.

 

第2の問題は,帰無仮説が本質的に正しい場合というのを考えにくいということである.統計学を用いる対象というのは,物理学の対象のように数理モデルでは原因を説明できない程,全ての要素が互いに影響し合っているような複雑なシステムであることが多い.そんなシステムで完全に独立した因子なんて存在するのだろうか?

それを考慮すると,帰無仮説として大抵挙げられる「2群間の差が完全にゼロ」という仮定がいかに非現実的かが解るだろう.つまり多くの場合,p値をほぼ間違っていることが確かな仮説から計算していることになる.こんな非現実的な帰無仮説を棄却したところでなんの意味があるのだろうか?つまり批判として出てくるのは,「そりゃ何かを変えればちょっとは結果に影響出るだろうよ」である.

 

ちなみに今回のケースでは,スクフェスのシステムの中に確率を決めている何らかの定数があるはずなので,(スクフェスの運営があくどい場合は)帰無仮説が厳密に正しい可能性が捨てきれない.従って今回に限っては,帰無仮説として「金銀UPに全く効果がない」を立てての検証には意味がある.

 

参考文献

今すごく眠いのであとで載せます.ごめんなさい.