特徴点(SIFT)のシリアライズとその他もろもろ

画像間類似度を計算する方法を調査してるんですが,画像の枚数が多くなってくると毎回SIFTを計算するのも辛くなってくるので,結果を保存する方法を調べることにしました.このトピックに限らずですが,参考にしたのはTheiaSfMというオープンソースプロジェクトです.githubのリンクは最後に載せてます.

X.Cereal

boost とか,いくつかシリアライズのためのライブラリがあるみたいなんですが TheiaSfM で Cereal が使われていたこともあって,ひとまず使ってみることにしました.ドキュメントも用意してくれていて,とても親切な感じですm(_ _)m

簡単なチュートリアル
cereal Docs - Main

githubのリンク
github.com

X.今回のシリアライズ対象.

で,今回シリアライズしてファイル保存したい内容ですが,SIFTのキーポイントと識別子になります.キーポイントの定義は,,,

class KeyPoint {
 public:
  KeyPoint() {}
  KeyPoint(float x, float y, float size, float angle, float responce, int octave)
      : x_(x), y_(y), size_(size), angle_(angle), responce_(responce), octave_(octave) {}

  bool operator==(const KeyPoint& rhs) const {
    return x_ == rhs.x_ && y_ == rhs.y_ && size_ == rhs.size_ && angle_ == rhs.angle_ &&
           responce_ == rhs.responce_ && octave_ == rhs.octave_;
  }

  bool operator!=(const KeyPoint& rhs) const { return !this->operator==(rhs); }

  friend std::ostream& operator<<(std::ostream& output, const KeyPoint& kp) {
    output << kp.x_ << ", " << kp.y_ << ", " << kp.size_ << ", " << kp.angle_ << ", "
           << kp.responce_ << ", " << kp.octave_;
    return output;
  }

 private:
  float x_;
  float y_;
  float size_;
  float angle_;
  float responce_;
  int octave_;

  friend cereal::access;
  template <class Archive>
  void serialize(Archive& ar) {
    ar(x_, y_, size_, angle_, responce_, octave_);
  }
};

で,識別子の定義は Eigen と STLVector を使って表現します.

std::vector<Eigen::VectorXf>& descriptors

X.Cerial をつかって自前のクラス(KeyPoint)を保存する.

上記で定義した KeyPoint クラスを Cereal を使って Serialize する方法ですが,これはとても簡単で,上記クラス定義の中の下記の関数を記述してあげればOKです.

  friend cereal::access;
  template <class Archive>
  void serialize(Archive& ar) {
    ar(x_, y_, size_, angle_, responce_, octave_);
  }

X.Cerial をつかって Eigen を保存する.

こちらがちょっとむずかしくて理解するのに時間がかかったんですが,シリアライズしたいオブジェクトを引数に取る "load" と "save" という関数を定義します.

template <class Archive, class _Scalar, int _Rows, int _Cols, int _Options, int _MaxRows,
          int _MaxCols>
inline typename std::enable_if<traits::is_output_serializable<BinaryData<_Scalar>, Archive>::value,
                               void>::type
save(Archive& ar, Eigen::Matrix<_Scalar, _Rows, _Cols, _Options, _MaxRows, _MaxCols> const& m) {
  int32_t rows = m.rows();
  int32_t cols = m.cols();
  ar(rows);
  ar(cols);
  ar(binary_data(m.data(), rows * cols * sizeof(_Scalar)));
}

template <class Archive, class _Scalar, int _Rows, int _Cols, int _Options, int _MaxRows,
          int _MaxCols>
inline typename std::enable_if<traits::is_input_serializable<BinaryData<_Scalar>, Archive>::value,
                               void>::type
load(Archive& ar, Eigen::Matrix<_Scalar, _Rows, _Cols, _Options, _MaxRows, _MaxCols>& m) {
  int32_t rows;
  int32_t cols;
  ar(rows);
  ar(cols);
  m.resize(rows, cols);
  ar(binary_data(m.data(), static_cast<std::size_t>(rows * cols * sizeof(_Scalar))));
}

上記のコードは TheiaSfM から持ってきたんですが,Eigen のコンストラクタが取るテンプレートパラメータを "load" と "save" に全て引き継がせることで,任意の Eigen::Matrix をシリアライズ/デシリアライズできるようにしてます.で,なおかつ std::enable_if を用いることで,Eigen::Matrix の中身の要素が Ceral によってバイナリ化できるかどうか調べてます.なので,,,バイナリ化できなければコンパイル時に関数が定義されず,エラーになる...はず...

C++得意な人って,このあたりって「息をする」用にコーディングできるんですかね....牛歩プログラマとしては羨ましい限りです.

X.実験に使ったコード

Cereal の github も submodule として取り込んでいるので,下記のコマンドを叩いてもらえば動くとこまでは行きます.

git clone --recurse-submodules  git@github.com:koichiHik/blog_cereal_use.git
sh build.sh
./build/cereal_sample 

github.com

X.参考文献&プロジェクト

TheiSfM
github.com

X.実際のコード

で,下記が実験用のコードです.単純にシリアライズ/デシリアライズして入出力前後のデータが一致していることを見てるだけですが,,,,


// STL
#include <cstdio>
#include <fstream>
#include <iostream>
#include <ostream>
#include <string>
#include <vector>

// Cereal
#include <cereal/archives/portable_binary.hpp>
#include <cereal/types/vector.hpp>

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

// Eigen
#include <eigen3/Eigen/Core>

// OpenCV
#include <opencv2/features2d/features2d.hpp>

// Original
#include "eigen_serializable.h"

class KeyPoint {
 public:
  KeyPoint() {}
  KeyPoint(float x, float y, float size, float angle, float responce, int octave)
      : x_(x), y_(y), size_(size), angle_(angle), responce_(responce), octave_(octave) {}

  bool operator==(const KeyPoint& rhs) const {
    return x_ == rhs.x_ && y_ == rhs.y_ && size_ == rhs.size_ && angle_ == rhs.angle_ &&
           responce_ == rhs.responce_ && octave_ == rhs.octave_;
  }

  bool operator!=(const KeyPoint& rhs) const { return !this->operator==(rhs); }

  friend std::ostream& operator<<(std::ostream& output, const KeyPoint& kp) {
    output << kp.x_ << ", " << kp.y_ << ", " << kp.size_ << ", " << kp.angle_ << ", "
           << kp.responce_ << ", " << kp.octave_;
    return output;
  }

 private:
  float x_;
  float y_;
  float size_;
  float angle_;
  float responce_;
  int octave_;

  friend cereal::access;
  template <class Archive>
  void serialize(Archive& ar) {
    ar(x_, y_, size_, angle_, responce_, octave_);
  }
};

void CreateKeyPointAndDescriptorPair(int num_data, std::vector<KeyPoint>& keypoints,
                                     std::vector<Eigen::VectorXf>& descriptors) {
  keypoints.clear();
  descriptors.clear();

  for (int i = 0; i < num_data; i++) {
    KeyPoint kp(1.1 * i, 1.1 * 10 * i, 1.1 * 100 * i, 1.1 * 1000 * i, 1.1 * 10000 * i,
                1.1 * 100000 * i);
    Eigen::VectorXf vec(4);
    vec << 1.1 * i, 1.1 * 10 * i, 1.1 * 100 * i, 1.1 * 1000 * i;
    keypoints.push_back(kp);
    descriptors.push_back(vec);
  }
}

void SaveAsBinary(const std::string& filepath, const std::vector<KeyPoint>& keypoints,
                  const std::vector<Eigen::VectorXf>& descriptors) {
  std::ofstream feature_writer(filepath, std::ios::out | std::ios::binary);
  cereal::PortableBinaryOutputArchive output_archive(feature_writer);
  output_archive(keypoints, descriptors);
}

void LoadFromBinary(const std::string& filepath, std::vector<KeyPoint>& keypoints,
                    std::vector<Eigen::VectorXf>& descriptors) {
  keypoints.clear();
  descriptors.clear();

  std::ifstream feature_reader(filepath, std::ios::in | std::ios::binary);
  cereal::PortableBinaryInputArchive input_archive(feature_reader);

  input_archive(keypoints, descriptors);
}

bool Compare(const std::vector<KeyPoint>& org_keypoints,
             const std::vector<Eigen::VectorXf>& org_descriptors,
             const std::vector<KeyPoint>& loaded_keypoints,
             const std::vector<Eigen::VectorXf>& loaded_descriptors) {
  for (int i = 0; i < org_keypoints.size(); i++) {
    if (org_keypoints[i] != loaded_keypoints[i] || org_descriptors[i] != loaded_descriptors[i]) {
      return false;
    }
    std::cout << std::endl;
    std::cout << org_keypoints[i] << std::endl;
    std::cout << loaded_keypoints[i] << std::endl;
    std::cout << org_descriptors[i] << std::endl;
    std::cout << loaded_descriptors[i] << std::endl;
  }
  return true;
}

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

  // X. Setting.
  const std::string test_path = "./sample.bin";
  int num_data = 10;
  std::vector<KeyPoint> keypoints;
  std::vector<Eigen::VectorXf> descriptors;

  // X. Create keypoints and descriptors.
  CreateKeyPointAndDescriptorPair(10, keypoints, descriptors);

  // X. Serialize and save to file.
  SaveAsBinary(test_path, keypoints, descriptors);

  // X. Load from file and deserialize.
  std::vector<KeyPoint> loaded_keypoints;
  std::vector<Eigen::VectorXf> loaded_descriptors;
  LoadFromBinary(test_path, loaded_keypoints, loaded_descriptors);

  // X. Compare if the results are same.
  if (!Compare(keypoints, descriptors, loaded_keypoints, loaded_descriptors)) {
    LOG(INFO) << "Contents are not the same!";
  } else {
    LOG(INFO) << "Contents are the same!";
  }

  // X. Remove temporary file.
  std::remove(test_path.c_str());

  return 0;
}