【Android】お手軽VR画像撮影・体験 のバックアップ(No.1)


100均とスマホでVR画像撮影

今ではある程度当たり前になってきたVR技術ですが、それでも自分でVR画像を撮影したりするのはあまり一般的ではありません。
鑑賞するだけなら100均の簡易なゴーグルでもいいのですが、自分で撮影するとなるとRICOH THETAのような専用デバイスが必要になってきます。

……本当にそうでしょうか?
考えてみればRICOH THETAは広角の魚眼レンズを前後に張り付けただけのものです。幸い魚眼レンズなら100均で手に入ります。スマホで魚眼画像をVR表示するソフトもVR Media Playerなどがあります。

というわけで試してみました。

VR Media Playerで写真を選択して、キャン★ドゥの魚眼レンズの場合LensはFisheye3、3DはNon-3DにしてVR表示をすると、若干反応が遅いもののそれなりにVRな体験ができました。

魚眼画像の正距円筒画像変換

魚眼のままVR表示できるのは楽でいいのですが、他のソフトでの表示なども考えると、正距円筒画像に変換しておきたくなります。
ひとまずstackoverflowで見つけたコード。正方形でないとうまく動かないようです。
いずれきちんとしたものを作りましょう。

using OpenCvSharp;
using System;

namespace FisheyeTest
{
   class Program
   {
       const string PATH_IMAGE = "fisheyetest.jpg";
       const int ESC = 27;
       static void Main(string[] args)
       {
           Console.WriteLine("Hello World!");
           Mat fisheyeImage, equirectangularImage;
           int Wf, Hf;
           float FOV;
           int We, He;

           fisheyeImage = Cv2.ImRead(PATH_IMAGE);
           Cv2.NamedWindow("Fisheye Image");
           Cv2.ImShow("fisheye Image", fisheyeImage);

           Wf = fisheyeImage.Size().Width;
           Hf = fisheyeImage.Size().Height;
           FOV = (180 * (float)Math.PI) / 180;

           We = Wf;
           He = Hf;

           while (Cv2.WaitKey(0) != ESC)
           {

           }
           equirectangularImage = new Mat();
           equirectangularImage.Create(He, We, MatType.CV_8UC3);

           for (int Xe = 0; Xe < We; Xe++)
           {
               for (int Ye = 0; Ye < He; Ye++)
               {
                   Point2f fisheyePoint = findCorrespondingFisheyePoint(Xe, Ye, We, He, FOV);
                   if (fisheyePoint.X >= We || fisheyePoint.Y >= He)
                       continue;

                   if (fisheyePoint.X < 0 || fisheyePoint.Y < 0)
                       continue;
                   //equirectangularImage.At<Vec3b>(Xe, Ye) = fisheyeImage.At<Vec3b>((int)fisheyePoint.X, (int)fisheyePoint.Y);
                   equirectangularImage.Set<Vec3b>(Xe,Ye, fisheyeImage.At<Vec3b>((int)fisheyePoint.X, (int)fisheyePoint.Y));
               }
           }

           Cv2.NamedWindow("Equirectangular Image");
           Cv2.ImShow("Equirectangular Image", equirectangularImage);

           while (Cv2.WaitKey(0) != ESC)
           {

           }

           Cv2.ImWrite("im2.jpg", equirectangularImage);
       }

       static Point2f findCorrespondingFisheyePoint(int Xe, int Ye, int We, int He, float FOV)
       {
           Point2f fisheyePoint = new Point2f();
           float theta, phi, r;
           Point3f sphericalPoint = new Point3f();

           theta = (float)(Math.PI * (Xe / ((float)We) - 0.5));
           phi = (float)(Math.PI * (Ye / ((float)He) - 0.5));

           sphericalPoint.X = (float)(Math.Cos(phi) * Math.Sin(theta));
           sphericalPoint.Y = (float)(Math.Cos(phi) * Math.Cos(theta));
           sphericalPoint.Z = (float)Math.Sin(phi);

           theta = (float)Math.Atan2(sphericalPoint.Z, sphericalPoint.X);
           phi = (float)Math.Atan2(Math.Sqrt(Math.Pow(sphericalPoint.X, 2) + Math.Pow(sphericalPoint.Z, 2)), sphericalPoint.Y);
           r = ((float)We) * phi / FOV;

           fisheyePoint.X = (int)(0.5 * ((float)We) + r * Math.Cos(theta));
           fisheyePoint.Y = (int)(0.5 * ((float)He) + r * Math.Sin(theta));

           return fisheyePoint;
       }
   }
}

参考リンク