聞きかじりめも

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

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に全く効果がない」を立てての検証には意味がある.

 

参考文献

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

スクフェスで学ぶ確率・統計(ベイズ推定編)

今回はスクフェスのイベント「メドレーフェスティバル(以下メドフェス)」を題材にして,統計の基礎を学ぼうという企画です.ベイズ統計の対象としては,離散値を扱うので非常に単純ですが勉強にはちょうどいいでしょう.ちなみに私は統計に関しては初歩的な知識しかなく,ベイズ統計は今回初めて触りました.以下の説明でも間違っているところがあるかもしれない.

今回のお題

メドフェスで金報酬銀報酬銅報酬が出る確率p_g, p_s, p_cをそれぞれ見積もりたい.n番目の報酬E_nを収集した時点でのそれぞれの確率を推定せよ.

理論

今回のケースでは確率分布の形,即ち母数に対する推定を行いたいため,次に示す母数分布に対するベイズの定理を利用する.

f:id:Mzawa2:20150309033734p:plain

今回の計算に利用する尤度関数f(E_n|θ)について,報酬は金(g),銀(s),銅(c)の3種であるから,3次の多項分布に従う.f:id:Mzawa2:20150309033955p:plain

事前に確率に関する情報を得られていないため,理由不十分の原則に従い,事前分布p(θ)は一様分布

f:id:Mzawa2:20150309034358p:plain

に従うものとしたいところだが,これだと最尤推定法と変わらない結果になってしまう.上の定理に代入するとわかるが,事後分布と尤度関数が比例してしまう.何が問題かというと,最尤推定法では「金が全く出なかった場合」などの特殊な場合に対応できない(確率0となってしまう.この現象をゼロ頻度問題という).そこでもっと常識的な推定をするため,事前分布と事後分布が変わらないような分布を仮定してあげることで解決する.これを自然共役分布といい,K=3の多項分布の場合には3次のDirichlet分布

f:id:Mzawa2:20150309034848p:plain

となる.事後分布は尤度関数×事前分布であるから,

 

f:id:Mzawa2:20150309040542p:plain

である.

(2015/9/7補足:読み返しているうちに「なんでガンマ関数の中に何の説明もなく多項分布のn_k!がねじ込まれてんだよおかしいだろ」と上式に疑問が生じましたが、よくよく考えてみたら重要なのは確率変数p_kの部分だけなので、あとのガンマ関数部分はDirichlet分布に無理矢理合わせるための単なる規格化定数と考えられます(多項分布のn_k部分が1段目に含まれていないのも同じ理由)。つまり上式の2段目は=ではなく∝で結ばれるべきです。)

このように自然共役分布を使うと事前分布と事後分布がパラメータのみ異なる同じ関数で表される.則ち,報酬が1個増えるごとにDirichlet分布の対応するパラメータがα_k→α_k +1とベイズ更新されることになる.たとえばE_i=Cだった場合,α_c→α_c +1という風に銅報酬に対応する分布のパラメータのみが更新される.なお,Dirichlet分布の主要な統計量は次の通り.

f:id:Mzawa2:20150309040843p:plain

 

 

ちなみに,以上の話の結果だけを抽出すると,ゼロ頻度問題を解決するためのLaplaceスムージング

f:id:Mzawa2:20150309041018p:plain

が,実は全ての初期値α_kが一定であるという仮定を入れたDirichlet分布を事前分布としたベイズ推定で算出した期待値と一致する,という結果が得られる.

やってみた

さて,実際に自分のログを元にExcelベイズ推定してみる.最終結果だけを言うと,全132試行のうち,金が4回,銀が38回,銅が90回出現した.ただし自分のログでは,1回のメドフェスで2個以上貰える報酬も別々の試行としてカウントしており,友人サポートで貰える報酬も1試行としている.また当然ながら,金銀報酬UPのサポートは一切つけていない.

単純に割り算で確認すると,p_g=0.030, p_s=0.288, p_c=0.682である.まあこれでも悪い推定ではないのだが,せっかくなのでnが小さい時の途中途中の推定確率も出してみよう.

1. 事前分布パラメータの決定

パラメータが1より小さいとそのパラメータに付随する確率の最頻値がマイナス値をとってしまい,1だと最尤推定法と同様ゼロ頻度問題が生じるので,パラメータは1より大きくすべきである.また,金報酬は確率が非常に小さいこと,金銀銅の順に確率が大きくなることを考慮して,

      α_g=1.05, α_g=2.35, α_g=3.65

とした.小数点以下は(両方の意味で)適当に決めた.

2. 報酬によるパラメータのベイズ更新

小難しい理論を並べたが,結局は上述したように,金が出たらα_g → α_g + 1,銀が出たらα_s → α_s + 1,銅が出たらα_c → α_c + 1とカウントしていくだけである.つまり今回のログでは,最終段階で

      α_g=5.05, α_g=40.35, α_g=93.65

となる.

3. 各時点での統計量の計算

今回は事前分布,事後分布にDirichlet分布を仮定しているので,上述したDirichlet分布の統計量の公式に当てはめるだけで計算が終わる.則ち,最終段階での金報酬の平均値,最頻値,分散はそれぞれ,

f:id:Mzawa2:20150309043016p:plain

である.銀,銅の場合も計算は同様.

4. グラフ化

事後確率の最頻値および平均値のベイズ推定値は以下のように収束していく.どうやら,金報酬のようにごく小さい確率では平均値が最頻値を上回る傾向にあり,金をとった瞬間に一気に上がってその後指数関数的に減少する,という動きになるようだ.当然ながら,最初の方は情報が少ないため推定値が荒れている.なお分散は,回数を重ねるたびに0に近づいていくという至極当たり前の結果が得られた.本当はルート取って標準偏差にすべきなんだけどだいたいの傾向を見るにはこれで十分.

f:id:Mzawa2:20150309043136p:plain

f:id:Mzawa2:20150309043146p:plain

 

番外編

さて先程は確率分布の統計量で推定したが,統計量だけだと全体像が見えづらいのでDirichlet分布を直接見てしまいたいという欲求に駆られる.幸いにして今回の調査対象は金・銀・銅の3種類だったので,全体の分布を直接2次元プロット可能である.「えっ3次元+分布の4次元じゃんどうすんの?」って一瞬思ったかもしれないが,よくよく見ると確率の総和が1であるという拘束条件を使って3次元に落とし込める.今回特に知りたいのは金・銀の確率なので,これらを先頭行・列に持ってきて0~1まで自分の知りたい分解能で並べ,中のマス目をDirichlet分布の式

f:id:Mzawa2:20150309043622p:plain

に従って計算する.ただしExcelで計算する場合は,オーバーフロー・アンダーフローが怖いので対数をとってから計算するのが無難.なお,計算結果は当然p_g + p_s ≧ 1の箇所とどちらかが0になる箇所は未定義となる(確率0の地点は対数を経由しなければ0として出る).計算結果は以下のグラフの通り.等高線が高い場所ほどそれらしき場所である.

 

f:id:Mzawa2:20150309043652p:plainf:id:Mzawa2:20150309043647p:plain

結論

金報酬が出る確率...約3%

銀報酬が出る確率...約30%

銅報酬が出る確率...約67%

が尤もらしいことが今回の結果から分かりました.次回報酬UPに本当に差があるのかを統計的に明らかにしたいと思います.

参考文献

主に以下のサイトでベイズ推定について学びました.この場を借りて感謝します.

ベイズ統計入門 

プログラマの為の数学勉強会 

説明が丁寧で例題も多く,非常にわかりやすかったです.

従来の推定法とベイズ推定法の違い | Sunny side up!

ベイズ主義の考え方がよくわかりました.

歪んだサイコロでベイズ - Negative/Positive Thinking 

ほぼ同じ問題だったので具体的な解き方について非常に参考になりました.

Bernoulli distribution and multinomial distribution (ベルヌーイ分布と多項分布) 

Bernoulli分布と多項分布の関係がよくわかりました.

ディリクレ分布まとめ - あらびき日記

Dirichlet分布で感じた疑問に的確な答えを見せてくれました.

http://hosho.ees.hokudai.ac.jp/~kubo/ce/2013/kubo2013esj.pdf 

統計的な態度がどうあるべきかが詳しく書いてあり,非常に勉強になりました.

 

(2015/9/7追記)

LDAでは何故ディリクレ分布を仮定するのか - SELECT * FROM life;

多項分布×Dirichlet分布の疑問が晴れました。

XAudio2をC++/CLIで使いたい 実装編

前回はXAudio2(というかSlimDX)を使う上での注意事項を散々書いてきたので,いよいよ今回は実装に移ります.C++/CLIの自分用覚書も兼ねてるので当たり前のことも書いています.

あんまり一般的な書き方じゃない気もしますが,とりあえず動くものを作ってみました.実行するとボタンが1つだけあるウインドウが現れて,ボタンを押すと押した瞬間にwave形式の波形データをメモリ上に作り,作ったデータを即再生します.

重要な部分をピックアップ

名前空間を宣言してるのでSlimDX::などは省略されていることに注意してください.

TestForm.h

 using namespace System;
    using namespace System::ComponentModel;
    using namespace System::Collections;
    using namespace System::Windows::Forms;
    using namespace System::Data;
    using namespace System::Drawing;

    using namespace SlimDX::XAudio2;
    using namespace SlimDX::Multimedia;

//----------------中略-----------------------

#pragma endregion
    public:
        XAudio2 ^device;
        MasteringVoice ^xMVoice;
        WaveFormat ^format;
        AudioBuffer ^xBuf;
        IO::MemoryStream ^mStream;
        SourceVoice ^xSrcVoice;
    private: 
        System::Void button1_Click(System::Object^  sender, System::EventArgs^  e);
        System::Void TestForm_Load(System::Object^  sender, System::EventArgs^  e);
        System::Void TestForm_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e);
    };
}
#include <Windows.h>

#define PI     3.1415926535f

using namespace RealtimeRendering;
using namespace std;

ヘッダファイルのSystem名前空間のところにしれっとSlimDXが混ざっています.これはXAudio2のハンドルをクラス内で宣言するのにこのあたりに書くのが都合がいいからです.また,ここでハンドルを宣言だけしておくとメンバ関数間で同じハンドルを使えます(つまり初期化と解放と処理が別々の関数で行なえる).ハンドルはpublicにしないとコンパイルエラーになります.最後のusingは別にTestForm.cppに書いてもいいんだけどなんとなく.

TestForm.cpp

 //XAudio2の初期化
    device = gcnew XAudio2();
    xMVoice = gcnew MasteringVoice(device);

フォームをロードしたときにXAudio2オブジェクトを生成します.SlimDXだからHRESULTのtry-catch例外処理で囲わずに作れるのが楽でいいですね.

    // 波形フォーマットの生成
    format = gcnew WaveFormat();
    format->FormatTag = static_cast<WaveFormatTag>(WAVE_FORMAT_PCM);     // フォーマット形式:PCM
    format->Channels = 1;                                                   // チャネル数:モノラル
    format->BitsPerSample = 16;                                             // 16 bit/sample
    format->SamplesPerSecond = 44100;                                       // サンプリングレート
    format->BlockAlignment = format->BitsPerSample / 8 * format->Channels;                // 1ブロックあたりのbyte数
    format->AverageBytesPerSecond = format->SamplesPerSecond * format->BlockAlignment; // 1秒あたりのバッファ数

PCMで波形フォーマットを生成します.簡単なモノラル波形を生成できればいいのでこのようにしました.ただしXAudio2とは異なり,FormatTagに定数を入れるときにSlimDXでは型が違うと怒られるのでcastしてやらなくてはいけません.

 // Sin波生成(double -> short)
    float a = 0.5;                // 振幅(0, 1]
    float freq = 880.0;           // 周波数[Hz]
    float time = 0.1;         // 時間[sec]
    array<short>^ waveData = gcnew array<short>((int)(format->AverageBytesPerSecond * time)); // t秒分のバッファ
    for (int i = 0; i < waveData->Length / 2; i++)
    {
        waveData[i] = (short)(SHRT_MAX * a * Math::Sin(i * PI * freq / (double)format->SamplesPerSecond));
    }
    // short -> byte(PCMフォーマットでメモリ上に波形データを展開)
    array<unsigned char>^ byteArray = gcnew array<unsigned char>(waveData->Length * format->BitsPerSample / 8);  // 16bit音源なので2倍
    Buffer::BlockCopy(waveData, 0, byteArray, 0, byteArray->Length);

波形データはarrayで確保しておきます.最初vectorでやろうとしたけど後でMemoryStreamを生成するときに型が違うと怒られました.本当は波の合成の都合上[-1, 1]の浮動小数点数で波形データを作ってからshortにcastしたかったんだけど,時間が無かったのでそれは次回にしよう.

PCMフォーマットでは8bitが基本ブロックなので,16bitモノラル音源では2byteのshortを1byteのunsigned charに分割しながら入れていかなきゃいけません.そこで大活躍するのがBuffer::BlockCopy()です.これを知る前は自力でビットシフトしながらforでコピーしていました.1行で書けるって素晴らしい…

 // バッファの作成
    xBuf = gcnew AudioBuffer();
    mStream = gcnew IO::MemoryStream(byteArray);
    xBuf->AudioData = mStream;
    xBuf->AudioBytes = (int)mStream->Length;
    xBuf->Flags = BufferFlags::EndOfStream;

AudioDataにどうやってデータを渡すかで苦労したのを憶えています.普通wavファイルから読み込むときはWaveStreamで何の苦も無くできたのですが,自分で波形を作るとなるとMemoryStreamにarrayを渡してそれを渡す,という格好になります.AudioBufferのメンバがどういうものなのか後でちゃんと調べないと.

 // Source Voice の作成
    xSrcVoice = gcnew SourceVoice(device, format);
    xSrcVoice->SubmitSourceBuffer(xBuf);

多分半永久的に音を生成し続けるようなコードになると最初にSourceVoiceオブジェクトを作った方がいいと思います.勘だけど.

 // 再生
    xSrcVoice->Start();
    MessageBox::Show("終了します.");

    // 終了
    xSrcVoice->Stop();

ボタンでStart(),Stop()メソッドだけを呼び出して,他の部分で波形生成・バッファ転送するような仕組みにしたいですが,今のところはこんな感じで.

 delete xBuf;
    delete xSrcVoice;
    delete mStream;
    delete xMVoice;
    delete device;

C++/CLIのハンドルはいつ解放されるかわからない(解放されないかもしれない)ので,明示的に解放します.
ちなみに今回のプログラムでは,こいつを怠ると実行時に

0xC0020001: その文字列結合は無効です。

なんていう見かけないエラーが出る時があります(出ないときもあるから厄介).

以下全ソースコード

RealtimeRendering

TestForm.h

#pragma once

namespace RealtimeRendering {

    using namespace System;
    using namespace System::ComponentModel;
    using namespace System::Collections;
    using namespace System::Windows::Forms;
    using namespace System::Data;
    using namespace System::Drawing;

    using namespace SlimDX::XAudio2;
    using namespace SlimDX::Multimedia;

    /// <summary>
    /// TestForm の概要
    /// </summary>
    public ref class TestForm : public System::Windows::Forms::Form
    {
    public:
        TestForm(void)
        {
            InitializeComponent();
            //
            //TODO: ここにコンストラクター コードを追加します
            //
        }

    protected:
        /// <summary>
        /// 使用中のリソースをすべてクリーンアップします。
        /// </summary>
        ~TestForm()
        {
            if (components)
            {
                delete components;
            }
        }
    private: System::Windows::Forms::Button^  button1;
    protected:

    private:
        /// <summary>
        /// 必要なデザイナー変数です。
        /// </summary>
        System::ComponentModel::Container ^components;

#pragma region Windows Form Designer generated code
        /// <summary>
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// </summary>
        void InitializeComponent(void)
        {
            this->button1 = (gcnew System::Windows::Forms::Button());
            this->SuspendLayout();
            // 
            // button1
            // 
            this->button1->Location = System::Drawing::Point(98, 121);
            this->button1->Name = L"button1";
            this->button1->Size = System::Drawing::Size(75, 23);
            this->button1->TabIndex = 0;
            this->button1->Text = L"button1";
            this->button1->UseVisualStyleBackColor = true;
            this->button1->Click += gcnew System::EventHandler(this, &TestForm::button1_Click);
            // 
            // TestForm
            // 
            this->AutoScaleDimensions = System::Drawing::SizeF(6, 12);
            this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font;
            this->ClientSize = System::Drawing::Size(284, 261);
            this->Controls->Add(this->button1);
            this->Name = L"TestForm";
            this->Text = L"TestForm";
            this->FormClosing += gcnew System::Windows::Forms::FormClosingEventHandler(this, &TestForm::TestForm_FormClosing);
            this->Load += gcnew System::EventHandler(this, &TestForm::TestForm_Load);
            this->ResumeLayout(false);

        }
#pragma endregion
    public:
        XAudio2 ^device;
        MasteringVoice ^xMVoice;
        WaveFormat ^format;
        AudioBuffer ^xBuf;
        IO::MemoryStream ^mStream;
        SourceVoice ^xSrcVoice;
    private: 
        System::Void button1_Click(System::Object^  sender, System::EventArgs^  e);
        System::Void TestForm_Load(System::Object^  sender, System::EventArgs^  e);
        System::Void TestForm_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e);
    };
}
#include <Windows.h>

#define PI     3.1415926535f

using namespace RealtimeRendering;
using namespace std;

TestForm.cpp

#include "TestForm.h"


//----------------------------------------
// XAudio2の初期化処理
//----------------------------------------
System::Void TestForm::TestForm_Load(System::Object^  sender, System::EventArgs^  e)
{
    //XAudio2の初期化
    device = gcnew XAudio2();
    xMVoice = gcnew MasteringVoice(device);

    // 波形フォーマットの生成
    format = gcnew WaveFormat();
    format->FormatTag = static_cast<WaveFormatTag>(WAVE_FORMAT_PCM);     // フォーマット形式:PCM
    format->Channels = 1;                                                   // チャネル数:モノラル
    format->BitsPerSample = 16;                                             // 16 bit/sample
    format->SamplesPerSecond = 44100;                                       // サンプリングレート
    format->BlockAlignment = format->BitsPerSample / 8 * format->Channels;                // 1ブロックあたりのbyte数
    format->AverageBytesPerSecond = format->SamplesPerSecond * format->BlockAlignment; // 1秒あたりのバッファ数

}
//----------------------------------------
// メモリ解放処理
//----------------------------------------
System::Void TestForm::TestForm_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e)
{
    delete xBuf;
    delete xSrcVoice;
    delete mStream;
    delete xMVoice;
    delete device;
}
//----------------------------------------
// Sin波生成直後に再生
//----------------------------------------
System::Void TestForm::button1_Click(System::Object^  sender, System::EventArgs^  e)
{
    // Sin波生成(double -> short)
    float a = 0.5;                // 振幅(0, 1]
    float freq = 880.0;           // 周波数[Hz]
    float time = 0.1;         // 時間[sec]
    array<short>^ waveData = gcnew array<short>((int)(format->AverageBytesPerSecond * time)); // t秒分のバッファ
    for (int i = 0; i < waveData->Length / 2; i++)
    {
        waveData[i] = (short)(SHRT_MAX * a * Math::Sin(i * PI * freq / (double)format->SamplesPerSecond));
    }
    // short -> byte(PCMフォーマットでメモリ上に波形データを展開)
    array<unsigned char>^ byteArray = gcnew array<unsigned char>(waveData->Length * format->BitsPerSample / 8);  // 16bit音源なので2倍
    Buffer::BlockCopy(waveData, 0, byteArray, 0, byteArray->Length);

    // バッファの作成
    xBuf = gcnew AudioBuffer();
    mStream = gcnew IO::MemoryStream(byteArray);
    xBuf->AudioData = mStream;
    xBuf->AudioBytes = (int)mStream->Length;
    xBuf->Flags = BufferFlags::EndOfStream;

    // Source Voice の作成
    xSrcVoice = gcnew SourceVoice(device, format);
    xSrcVoice->SubmitSourceBuffer(xBuf);

    // 再生
    xSrcVoice->Start();
    MessageBox::Show("終了します.");

    // 終了
    xSrcVoice->Stop();
}

main.cpp

//-------------------------------------------------------
//     XAudio2によるリアルタイムサウンド合成
//-------------------------------------------------------

#include "TestForm.h"

using namespace RealtimeRendering;

[STAThreadAttribute]
int main(array<System::String ^> ^args)
{
    Application::EnableVisualStyles();
    Application::SetCompatibleTextRenderingDefault(false);
    Application::Run(gcnew TestForm());
    return 0;
}