c++のシリアライザcerealをopencvのcv::Matに対応させた

c++シリアライズしたいなと思ったら、ヘッダーオンリーで使用でき、利用方法も簡単なcerealというオープンソースのライブラリを発見した。それをopencvのMatにも対応させたので紹介しよう。

github.com

cerealは利用方法が簡単であり、c++標準のコンテナであれば、vectorやmap, setなど大抵が対応している。自作のclassもsave()load()関数を実装することでシリアライズが可能になる。日本語の簡単なサンプルはここで紹介されているので、そこを見てほしい。

以下では簡単にsave(), load()関数の書き方を紹介する。要点は、ar関数でシリアライズしたいメンバ変数を指定してsave()関数とload()関数それぞれに書くだけだ。arの引数には異なる型のオブジェクトを複数入れることが出来るので、記述も簡単である。 以下は int a, float bという変数を持つData classにシリアライズのための関数を付け加えた例である。

#include <iostream>
#include <cereal/archives/binary.hpp>

class Data {
private:
  int a;
  float b = 0;
public:
  template <class Archive>
  void save( Archive & ar ) const {
    ar(a, b);
  }

  template <class Archive>
  void load( Archive & ar ) {
    ar(a, b);
    ar(a, b);
  }

次に、このserealでopenCVcv::Matシリアライズすることを考える。 少し調べるとboost::serializationとの比較をしつつ、cv::Matシリアライズ方法を紹介してくれている方法を発見した。これを見ると便利なことにseeralはスマートポインタポインタもシリアライズしてくれる。cv::Matの型を考慮する必要がないので、実装は簡単になっている。

template<class Archive>
void save(Archive& ar, const cv::Mat& mat)
{
    int rows, cols, type;
    bool continuous;

    rows = mat.rows;
    cols = mat.cols;
    type = mat.type();
    continuous = mat.isContinuous();

    ar & rows & cols & type & continuous;

    if (continuous) {
        const int data_size = rows * cols * static_cast(mat.elemSize());
        auto mat_data = cereal::binary_data(mat.ptr(), data_size);
        ar & mat_data;
    }
    else {
        const int row_size = cols * static_cast(mat.elemSize());
        for (int i = 0; i < rows; i++) {
            auto row_data = cereal::binary_data(mat.ptr(i), row_size);
            ar & row_data;
        }
    }
};


template<class Archive>
void load(Archive& ar, cv::Mat& mat)
{
    int rows, cols, type;
    bool continuous;

    ar & rows & cols & type & continuous;

    if (continuous) {
        mat.create(rows, cols, type);
        const int data_size = rows * cols * static_cast(mat.elemSize());
        auto mat_data = cereal::binary_data(mat.ptr(), data_size);
        ar & mat_data;
    }
    else {
        mat.create(rows, cols, type);
        const int row_size = cols * static_cast(mat.elemSize());
        for (int i = 0; i < rows; i++) {
            auto row_data = cereal::binary_data(mat.ptr(i), row_size);
            ar & row_data;
        }
    }
};

引用: https://www.patrikhuber.ch/blog/2015/05/serialising-opencv-matrices-using-boost-and-cereal/

しかし、この方法ではar()関数に直接cv::Matを入れることが出来ないので、自作のDataクラスのsave(), load()関数の中身には以下のように複数回シリアライズ関数を記述することになり、美しくない。形式も異なるのでなおさらである。

class Data {
private:
  int a;
  float b;
  cv::Mat mat;
public:
  template <class Archive>
  void load( Archive & ar ) {
    ar(a, b);
    load(ar, mat);
//     ar(a, b, mat); 本当はこう書きたい
  ...

そこで、上のcv::Matのcerealが標準で対応しているvectorやmapと同じ方法で対応させる。 cerealはcereal/include/cereal/types/以下にそれぞれの型のシリアライズ方法を実装しているので、それを参考に、上のcv::Matsave, load関数を書き換えた。

namespace cereal {
//! Saving for cv::Mat
template <class Archive> inline
void CEREAL_SAVE_FUNCTION_NAME(Archive& ar, const cv::Mat& mat)
{
  int rows, cols, type;
  bool continuous;

  rows = mat.rows;
  cols = mat.cols;
  type = mat.type();
  continuous = mat.isContinuous();

  ar & rows & cols & type & continuous;

  if (continuous) {
    const int data_size = rows*cols*static_cast<int>(mat.elemSize());
    auto mat_data = cereal::binary_data(mat.ptr(), data_size);
    ar &mat_data;
  } else {
    const int row_size = cols*static_cast<int>(mat.elemSize());
    for (int i = 0; i < rows; i++) {
      auto row_data = cereal::binary_data(mat.ptr(i), row_size);
      ar &row_data;
    }
  }
}

//! Loading for cv::Mat
template <class Archive> inline
void CEREAL_LOAD_FUNCTION_NAME(Archive& ar, cv::Mat& mat)
{
  int rows, cols, type;
  bool continuous;

  ar & rows & cols & type & continuous;

  if (continuous) {
    mat.create(rows, cols, type);
    const int data_size = rows * cols * static_cast<int>(mat.elemSize());
    auto mat_data = cereal::binary_data(mat.ptr(), data_size);
    ar & mat_data;
  }
  else {
    mat.create(rows, cols, type);
    const int row_size = cols * static_cast<int>(mat.elemSize());
    for (int i = 0; i < rows; i++) {
      auto row_data = cereal::binary_data(mat.ptr(i), row_size);
      ar & row_data;
    }
  }
}
} // namespace cereal

上のものをヘッダに付け加えることで、テンプレートの解決の際にcv::Mat 型を自動で処理してくれるようになる。 これより以下のようにシンプルに書けるようになった。

class Data {
private:
  int a;
  float b;
  cv::Mat mat;
public:
  template <class Archive>
  void load( Archive & ar ) {
  ar(a, b, mat);
//     ar(a, b);
//     load(ar, mat);
  ...