Insta 360 One X

ここ何回かのエントリで書いている魚眼レンズキャリブレーションの件なんですが,Insta 360 One X っていう 360°カメラをキャリブレーションするのに使いました.国際通りの三次元復元に向けて以前購入した Sony の α7 2 で撮影しようとてたんですが,画角が狭く撮影が終わらないのと,ガジェット欲しさに買ってしまいました.

Appleのウェブサイトから購入.

いつもの如くAmazonでポチッとしかけていたんですが,AppleのWebサイトでも販売していることを発見.自分が購入した時はAppleのサイトが一番安かったです.この値段でバッテリーが二個,SDカード32GB,撮影用の自撮り棒もついてます.
www.apple.com

f:id:rkoichi2001:20190611225758j:plain
箱が思いっきりAppleの雰囲気を漂わせてますが,Apple仕様なんですかね.

出力ファイルフォーマット

映像・ビデオともにDual Fisheyeの形で出てきます.で,フォーマットに関しては,

写真
・RAW撮影オプションON:”.dng”というアドビのイメージフォーマット,".insp" という Insta360のイメージフォーマットの2つのファイルが出力されます.
・RAW撮影オプションOFF:”.insp” というフォーマットのみ出てきます.

ビデオ
・LOG撮影オプションON/OFFともに:".insv" というフォーマットで出てきます.

".insp" にしても,".insv" にしても,拡張子を ".insp" -> ".jpg", ".insv" -> ".mp4" にしてあげれば普通のソフトで見ることができました.が,ビデオに関しては流石に写真よりはだいぶ画質が落ちていると思います.あと,UVCデバイスとしてロボコンで使えないかな〜とちょっと淡い期待を抱いていたのですが,USB経由で画像を送信することはできなさそうでした.USBのストレージとしてしか認識されてないように思います.

uvcdynctrl --list

で確認しましたが,認識されてませんでした.

撮影画像

自分は国際通りを撮影することが目的だったので魚眼のままの画像が必要だったんですが,前述の通り,写真もビデオも魚眼の映像のまま出てきます.おそらくカメラでは撮影しているだけで,その後の処理はPCやスマホですることを前提としているのだと思います.下の写真が編集ソフトです.

f:id:rkoichi2001:20190612011251p:plain
Insta 360 Studio

撮影した写真

下記,Insta 360 One X で撮影した写真をそのまま載せてます.これをPCで事後処理して全天球画像に変換します.

f:id:rkoichi2001:20190611231107j:plain
Insta360 One X の撮影画像(まんまです.)

本当はビデオを撮影していたら You Tube にアップして 360° ビデオみたいにできたんですが,ちょっと手持ちが写真だけだったので....Insta 360 Studio を使えば,自由に視点変換した写真を作ることができます.

f:id:rkoichi2001:20190611235209j:plain
地平線が円周になってます.

f:id:rkoichi2001:20190611235301j:plain
自撮り棒は後処理で消してくれます(笑)

画角の比較

Sony α7 2 に付属していたレンズと Insta でとった写真の比較です.Insta でとった写真は画角が 180°以上有ります.α2 は一眼レフなんで,「広角レンズ買えばええやん?」と言われればそれはそのとおりなんですけども(笑)

Insta360 One X で撮影した写真

f:id:rkoichi2001:20190611232920j:plain
部屋の様子 with 魚眼レンズ

手持ちカメラ with デフォルトレンズで撮影した写真

f:id:rkoichi2001:20190612005851j:plain
Sony α2 with デフォルトレンズで撮影

.dng ファイルの変換コード

で,せっかく Raw の画像ファイルを出力してくれるので, Raw のファイルを読み込んでくれるライブラリを探してたんですが,有りました.
www.libraw.org

Ubuntu のパッケージとしてインストールして使えます.

sudo apt install libraw-dev 

下記,.dng ファイルをlibraw でデコードして jpg で保存します.1つの .dng ファイルに2つの魚眼カメラの画像が含まれているので,分割してます.

// Standard
#include <iostream>
#include <string>

// Boost
#include <boost/filesystem.hpp>

// Gflags
#include <gflags/gflags.h>

// Glog
#include <glog/logging.h>

// OpenCV
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

// LibRaw
#include <libraw/libraw.h>

// Original
#include <fileutil/filesystem_util.h>

DEFINE_string(picture_input_dir, "",
              "Path to the directory containing the insta \".dng\" files.");
DEFINE_string(picture_output_dir, "",
              "Output directory for converted \".jpg\" files");

using namespace std;

namespace {

bool ConvertDngFileToJpgFile(const std::string &path,
                             const std::string &save_dir_path) {
  LibRaw raw_processor;

  // Setting
  raw_processor.imgdata.params.use_camera_matrix = 1;
  raw_processor.imgdata.params.use_camera_wb = 1;

  CHECK(raw_processor.open_file(path.c_str()) == LIBRAW_SUCCESS)
      << "LibRaw can not open file. : " << path;
  CHECK(fileutil::CheckDirectory(save_dir_path))
      << "Directory specified for output is not valid. : " << save_dir_path;

  raw_processor.unpack();
  raw_processor.dcraw_process();
  libraw_processed_image_t *img = raw_processor.dcraw_make_mem_image();
  cv::Mat insta_img(raw_processor.imgdata.sizes.height,
                    raw_processor.imgdata.sizes.width, CV_8UC3, img->data);

  cv::Mat insta_rgb;
  cv::cvtColor(insta_img, insta_rgb, cv::COLOR_RGB2BGR);

  int width = insta_img.cols, height = insta_img.rows / 2;
  int lower_start_row = height;
  cv::Mat insta_rgb_upper(insta_rgb, cv::Rect(0, 0, width, height));
  cv::Mat insta_rgb_lower(insta_rgb,
                          cv::Rect(0, lower_start_row, width, height));

  std::string filename;
  bool result = fileutil::ExtractFilename(path, filename);
  CHECK(result) << "Invalid file name.";
  size_t n = filename.rfind(".dng");
  std::string save_filename_f = filename + "  ";
  std::string save_filename_b = filename + "  ";
  save_filename_f.replace(n, 6, "_f.jpg");
  save_filename_b.replace(n, 6, "_b.jpg");

  std::string up_path = save_dir_path + "/" + save_filename_f;
  std::string low_path = save_dir_path + "/" + save_filename_b;
  vector<int> compression_params;
  compression_params.push_back(CV_IMWRITE_JPEG_QUALITY);
  compression_params.push_back(100);

  cv::imwrite(up_path, insta_rgb_upper, compression_params);
  cv::imwrite(low_path, insta_rgb_lower, compression_params);

  return true;
}
}  // namespace

int main(int argc, char **argv) {
  google::ParseCommandLineFlags(&argc, &argv, true);
  FLAGS_alsologtostderr = 1;
  google::InitGoogleLogging(argv[0]);

  CHECK_NE(FLAGS_picture_input_dir, "")
      << "Please specify the directory containing the .dng files.";
  CHECK(fileutil::CheckDirectory(FLAGS_picture_input_dir))
      << "Given input directory is not valid!";
  std::string output_dir = FLAGS_picture_output_dir != ""
                               ? FLAGS_picture_output_dir
                               : FLAGS_picture_input_dir + "/../converted";
  CHECK(fileutil::CreateDirectoryIfNotExist(output_dir))
      << "Output directory is not valid or able to be created.";

  vector<string> dng_filelist;
  LOG(INFO) << "Collecting all \".dng\" files in the specified directory...";
  fileutil::RaiseAllFilesInDirectory(FLAGS_picture_input_dir, dng_filelist,
                                     vector<string>{".dng"});

  LOG(INFO) << "Converting \".dng\" file to \".jpg\" files. Total files to "
               "convert is : "
            << dng_filelist.size();
  for (size_t idx = 0; idx < dng_filelist.size(); idx++) {
    if (idx % 10 == 0) {
      LOG(INFO) << "Processing : " << to_string(idx) << " / "
                << dng_filelist.size();
    }
    ConvertDngFileToJpgFile(dng_filelist[idx], output_dir);
  }

  return 0;
}

ということで,明日は久々の晴れっぽいので,撮影してきます(ノ´∀`*)