LiveTHETA

LiveTHETA.png


全天球カメラ RICOH THETA S が、延々とリアルタイムで動画撮影できる「ライブモード」に対応したので、それを FireMonkey 上でリアルタイムにグルグル視点を回しながら閲覧できるアプリを作ります。

※ THETA S をお持ちでない場合は、付属のフレーム画像でご体験下さい。

THETA S は USB 接続すると、単なる WEB カメラ と認識されるので、映像フレームは TCameraComponent で逐次取得できます。もっとも、USB 接続では画質が悪いので、性能を生かすなら、別途ビデオキャプチャデバイスを用いて HDMI 経由で取得するべきでしょう。

  • USB 接続: 1280×720 / 15fps
  • HDMI接続: 1920×1080 / 30fps

THETA S のライブ動画は、魚眼レンズからの生映像をそのまま二つ貼り合わせた Dual-Fisheye 形式で出力されます。


静止画撮影ですと、緯/経度で展開された Equirectangular 形式で出力されるので、球体にテクスチャマッピングするたけで閲覧できますが、今回は Dual-Fisheye 形式を Equirectangular 形式へ変換する処理が必要となります。


もっとも、RICOH はニコニコ動画でのストリーミング配信のために、スペックを強化して Equirectangular 形式で出力できる実験機も開発していますが、熱がまだ問題のようです。(^^;)


Dual-Fisheye 形式の魚眼の映像は、等距離射影:Equidistance Projection という座標変換で射影されています。中心からの半径がカメラの画角に直接比例している、最も一般的な形式です。国連のマークもこの図法ですね。


ただ、二つの魚眼レンズを組み合わせて360°の全天を作るからと言って、片方の画角がぴったり 180° というわけではなく、繋ぎ目の滑らかさを考慮してマージンが設けてあります。つまり、片方だけで 200°以上の画角を持っているわけですが、となると、180°に対応する半径は何ピクセルの所なのでしょうか?

RICOH に聞いても教えてくれなかったので、自分で調べてみました。簡単に言うとペンライトの輝点を映り込ませて、両方の魚眼における中心からの距離が等しくなる瞬間を狙います。さすればその位置が、両眼の光軸から 90° の位置ということになります。もっとも、正確に計るために、THETAからなるべく離れる必要があります。

11.JPG


というわけで、半径 425px の位置が両眼の境界であると判明しました。

THETA_Dual-Fisheye_1920x1080.png


仕様が判明したところで、シェーダを書いていきます。独自の TMaterialSource の作り方は前回の記事を参考に。

17.JPG


今回はピクセルシェーダで処理し、以下のような HLSL コードとなります。

TResultP MainP( TSenderP _Sender )
{
    TResultP _Result;

    const float VideoW = 1920.0;
    const float VideoH = 1080.0;
    const float Radius =  425.0;

    const float W1 = VideoW / 4.0;
    const float W3 = 3.0 * W1;

    float  L, A, B;
    float2 S, P, P1, P2, T1, T2;
    float3 V;
    float4 C1, C2;

    S.x = Pi2 * _Sender.Tex.x - Pi;
    S.y = P2i - Pi * _Sender.Tex.y;

    L   = cos( S.y );
    V.x = L * cos( S.x );
    V.y = L * sin( S.x );
    V.z = sin( S.y );

    A = asin( V.y ) / P2i;

    B = ( 1.0 + clamp( A / 0.02, -1.0, +1.0 ) ) / 2.0;

    P.x = V.x;
    P.y = V.z;
    P = Radius * normalize( P );

    P1 = ( 1 - A ) * P;
    P2 = ( 1 + A ) * P;

    T1.x = ( W1 - P2.y ) / VideoW;
    T1.y = ( W1 + P2.x ) / VideoH;

    T2.x = ( W3 + P1.y ) / VideoW;
    T2.y = ( W1 + P1.x ) / VideoH;

    C1 = _Texture.Sample( _SamplerState, T1 );
    C2 = _Texture.Sample( _SamplerState, T2 );

    _Result.Col = ( C2 - C1 ) * B + C1;

    _Result.Col = _Opacity * _Result.Col;

    return _Result;
}


あとはこれを TSphere に適用して中から覗くだけです。

procedure TForm1.FormCreate(Sender: TObject);
var
   VCD :TVideoCaptureDevice;
begin
     _Material := TMyMaterialSource.Create( Self );

     Sphere1.MaterialSource := _Material;

     with CameraComponent1 do
     begin
          VCD := SearchTHETAS;

          if Assigned( VCD ) then
          begin
               Device := VCD;

               Active := True;
          end
          else
          begin
               ShowMessage( 'THETA S が見つかりません。' );

               _Material.Texture.LoadFromFile( '..\..\_DATA\DualFisheye 1920x1080.png' );
          end;
     end;
end;

ちなみに、USB 接続の THETA は WEB カメラとして認識されるので、TCameraComponent での取得が容易ですが、複数の WEB カメラが接続されていた場合には、THETAの Device を探す必要があります。

function TForm1.SearchTHETAS :TVideoCaptureDevice;
var
   I :Integer;
   D :TCaptureDevice;
begin
     with TCaptureDeviceManager do
     begin
          if Current <> nil then
          begin
               for I := 0 to Current.Count - 1 do
               begin
                    D := Current.Devices[ I ];

                    if ( D.MediaType = TMediaType.Video ) and
                       ( D is TVideoCaptureDevice       ) then
                    begin
                         if D.Name = 'RICOH THETA S' then
                         begin
                              Result := TVideoCaptureDevice( D );

                              Exit;
                         end;
                    end;
               end;
          end;
     end;

     Result := nil;
end;

しかし、探せたところで、TCameraComponent の Device プロパティは書き込み不可なので、クラスヘルパーで改造します。

     HCameraComponent = class helper for TCameraComponent
     private
     protected
       function GetDevice_:TVideoCaptureDevice; inline;
       procedure SetDevice( const Device_:TVideoCaptureDevice );
     public
       property Device :TVideoCaptureDevice read GetDevice_ write SetDevice;
     end;

~~~~~     

function HCameraComponent.GetDevice_:TVideoCaptureDevice;
begin
     with Self do
     begin
          Result := GetDevice;
     end;
end;

procedure HCameraComponent.SetDevice( const Device_:TVideoCaptureDevice );
begin
     with Self do
     begin
          FDevice := Device_;
     end;
end;