milk_spoonのブログ

さじの情報科学的活動、考えたこと、その他を雑記するためのブログです。

OpenCV←→Siv3Dで画像のデータ変換をする

これはSiv3D Advent Calendar 2017 8日目の記事です。

最近OpenCVC++)で色々と画像処理をしてるけども、デフォルトのウィンドウとかキー入力受付が結構使うのが辛く、せっかくなので表示とか操作インターフェースを使い慣れたSiv3Dでやりたい。
Siv3Dの画像クラスであるs3d::Imageと、OpenCVの画像クラスであるcv::Matの変換ができれば、OpenCVで加工した画像をSiv3Dで表示する、ってことが普通にできそう。
やっぱり先駆者さんもいるもので、このコードを参考にちょいちょいしてみたらできた。

Siv3D+OpenCV環境

ちょろっと環境構築の話もしておく。
まず当たり前だけどVisual Studio 2015とSiv3D とOpenCV(3.0.0)のインストールは済ませておく。
作るプロジェクトはSiv3Dプロジェクト。
そのプロジェクトに、OpenCV3.0.0のincludeディレクトリを「追加のインクルードディレクトリ」として設定。終わり。
f:id:milk_spoon:20171207205940p:plain
追加のライブラリディレクトリなどの設定はいらず、Siv3D側で導入済みのOpenCVのライブラリを参照して動く。
簡単だけどcv::imreadやcv::imshowが通常通り動かない点など、通常の導入とちょっと違う点も出てくる。
例えば、cv::imreadはbmpしか読み込まないので、基本的に画像読み込みはs3d::Imageから行ったほうがいいと思う。
他、cv::imshow等ウィンドウ系関数が使えない。そもそもインターフェースをSiv3Dにするという話なので使わないのだけど…

コンパイルした時、未解決の外部シンボル "public: void __cdecl cv::Mat::copyTo~とかって怒られる場合は、OpenCV3.0.0以外のバージョンのincludeディレクトリが指定されているかも。
2017年12月現在の最新はOpenCV3.3.1なので、インストールもディレクトリの指定も間違わないように注意。

画像の変換

ということでcv::Mat←→s3d::Image。
以下のコードは
1. s3d::Imageでファイル読込→cv::Matに変換して加工→s3d::Imageを出力
2. cv::imreadでファイル読込→s3d::Imageに変換して加工→s3d::Imageを出力
をする。最後のループで両方の結果を表示。

下記コードを動かす前に、Siv3DデフォルトでExampleディレクトリに入ってる「Windmill.png」をbmpに変換した「Windmill.bmp」を同じディレクトリに置いておいてください。
ペイントとかで開いたのを保存しなおすとかでOKです。

#define NO_S3D_USING

# include <opencv2/opencv.hpp>// OpenCV3.0.0
# include <Siv3D.hpp>//Siv3D 2016Augustv2

void Main()
{
        //1. s3d::Image to cv::Mat
        s3d::Image image1(L"Example/Windmill.png");

        //s3d::Image内の画素データを参照するcv::Matを作成
        cv::Mat_<cv::Vec4b> mat1(image1.height, image1.width, static_cast<cv::Vec4b*>(image1.data()), image1.stride);

        //OpenCVの関数で加工 今回はネガポジ反転
        mat1 = ~mat1;

        for (auto it = mat1.begin(); it != mat1.end(); ++it)
        {
                //透明度も反転で0になってしまっているので255に
                (*it)[3] = 255;
        }

        //表示用        
        s3d::Texture texture1(image1);

        //2. cv::Mat to s3d::Image

        //cv::imreadはbmpしか読み込めない
        cv::Mat mat2 = cv::imread("Example/Windmill.bmp", cv::IMREAD_UNCHANGED);

        //channelが4かつ画素はRGBAの並びでないといけないので変換
        //(imreadのデフォルトはBGR)
        cv::cvtColor(mat2, mat2, cv::COLOR_BGR2RGBA);

        s3d::Image image2(mat2.cols, mat2.rows);

        //cv::Mat内の画素データをs3d::Image内へコピー
        std::memcpy(image2.data(), mat2.data, 4 * mat2.cols * mat2.rows);

        //加工
        image2.scale(0.3);

        //表示用
        s3d::Texture texture2(image2);

        while (s3d::System::Update())
        {
                texture1.draw();
                texture2.draw(texture1.width, 0);

        }
}

f:id:milk_spoon:20171112144050p:plain

1.でcv::Matを加工した後、s3d::Imageへの変換処理をしなくていいの?ってなるけど、今回はなくて大丈夫。
s3d::Image→cv::Matは「変換」と書いてるけど、s3d::Imageが持ってるデータをcv::Matが指すようになるだけなので。
当然、cv::Matを更新すると、s3d::Imageも更新されている。

初めに読み込んだイメージから別のcv::Matをcloneなどで作った場合は、cv::Mat→s3d::Imageの処理を挟む必要がある。

cv::Mat→s3d::Imageする際は、cv::Mat側がチャンネル数4で、画素がRGBAの並びでないといけない。
上記のようにmemcpyを使うと、cv::Matがメモリ上で連続データでないといけない。(ROIなど、連続データでないこともある。)
例では適当に変換しているが、本来変換元のcv::Matにどのようなものが来るかは色々あると思っていて、

  • s3d::Imageから変換したもの(チャンネル数4, RGBA)
  • cv::imreadでファイル読込したもの(チャンネル数2 or 3 or 4, グレイスケール or BGR)
  • 他のcv::Matを加工したもの(チャンネル数等不明、不連続データの可能性あり)

これらを適宜RGBAに変換などする必要がある。

変換関数

ここまで来て結構めんどくさいなーと思いました。cv::Mat→s3d::Imageするのに↑のような条件気にしたりとかはやってられない感。
なのでわりと思考停止で変換できるんじゃないかなーという変換関数たちを作った↓

github.com

↑の条件を気にしないでcv::Mat→s3d::Imageに変換したいって場合はcvsiv::MatToImageForceを呼んで渡せばだいたい変換できる。
(cv::MatがRGBかBGRかは判定できないので指定する必要アリ)
↑の条件に合っていることがわかっていればcvsiv::MatToImageを呼んだほうがコピーが少ないので良い。
cvsiv::IsConvertibleByMatToImage/IsConvertibleByMatToImageForceでそれぞれ適用可能かも判定できる。

上の例のようにs3d::Imageを参照するcv::Matを作りたいときはcvsiv::GetMatLinkedToImage。
むしろデータ共有してるとか嫌だからコピーしてcv::Mat作りたいときはcvsiv::ImageToMat。
ただ画像データは基本コピーしないほうがパフォーマンスいいと思うので…

まとめ

cv::Matの表現できる画像データはs3d::Imageより多様なので、任意のcv::Matをs3d::Imageへ変換しようとするのは意外と色々あって大変だけどもう変換関数があるので解決(?)
次はこれを使ってOpenCVで処理した画像をSiv3Dで見てみるようなものを作ってみたい。
→作りました
spoonblog.hatenablog.com


Siv3D Advent Calendar 2017 8日目でした。
次はhota1024さんです。