聞きかじりめも

主にC++やメディア処理技術などに関して気付いたことを書いていきます.ここが俺のメモ帳だ!

cv::Matにおけるclone()とcopyTo()の挙動の違い

※本記事はOpenCV Advent Calender 2016向けに書きました。 http://qiita.com/advent-calendar/2016/opencv

OpenCVをちょっと知ってる人はこのタイトルで「何言ってんだこいつ」ってなったかも知れませんが,一見同じに見える(※後述)この2種類のdeep-copyメソッドは,実はある特定のコードで挙動が異なります.

その特定のコードとはROI処理です.StackOverflowでも「ROIへのコピーはcopyTo()を使えばできるよ!」と書いてありましたが,なぜ(自分が)普段から使うclone()を使わないのかについて言及する人がいなかったので,今回の検証に繋がりました.

ROIに画像を貼り付けられない!?

例えば画像処理で顔検出して,そこにアニメアイコンを貼り付けるような自動クソコラ生成器を作ろうとする場合,ROIを設定してそこに既存の画像を貼り付ける,なんてことをすると思います.しかし,それを素直にcv::Mat::clone()メソッドで実装しようとするとなぜか貼り付けられない場合があります.

以下のコードは,次のような画像を作成しようとしているコードです.

f:id:Mzawa2:20161208205738p:plain

 // ROI
    Rect roi(100, 100, 100, 100);
    Mat img_roi = frame_c(roi);   // 元画像のROIを生成
    // clone
    Mat img_roi_clone = Mat(100, 100, CV_8UC3, Scalar(0, 255, 0));  // 貼り付ける画像を生成
    img_roi = img_roi_clone.clone();    // 貼り付ける画像をcloneしてからROIに貼り付け
    imshow("Color ROI clone", frame_c);    // なぜか貼り付けできてない

しかし,実際にこれで出てくる画像は・・・

f:id:Mzawa2:20161208205746p:plain

このように,なぜか貼り付けできません.通常,ROIへの任意の画像処理は元画像frame_cに即座に反映されるため,このようにimg_roiに画像データをコピーすればROIの部分だけ塗りつぶされるはずなのですが,そうはならないようです.

そこでcv::Mat::clone()ではなくcv::Mat::copyTo()を使うと,目的の画像のようになります.

 // ROI
    Rect roi(100, 100, 100, 100);
    Mat img_roi = frame_c(roi);   // 元画像のROIを生成
    // copyto
    Mat img_roi_copy = Mat(100, 100, CV_8UC3, Scalar(0,255,0));
    img_roi_copy.copyTo(img_roi);   // 貼り付ける画像をROIにコピー
    imshow("Color ROI copy", frame_c);   // こうすると貼り付けできる

f:id:Mzawa2:20161208230845p:plain

つまり,copyTo()でならROIに画像をコピーできるけど,clone()では画像をROIにコピーできないという仕様になっていることが確認できます.

※ちなみに,浅いコピー(shallow copy)img_roi = img_roi_shallow;は当然ながらうまくいきません.

やったね!今度からcopyTo()を使えばすべてうまくいくんだね!

更なる罠

…って簡単にはいかないのが困りものです.実は,一度「うまくいかないコピー」でROIをコピーしようとしてしまうと,その後いくらcopyTo()で正しく書こうが一切反映されなくなります.

    // ROI
    Rect roi(100, 100, 100, 100);
    Mat img_roi = frame_c(roi);
    // clone
    Mat img_roi_clone = Mat(100, 100, CV_8UC3, Scalar(0, 0, 255));
    img_roi = img_roi_clone.clone();
    imshow("Color ROI clone", frame_c);   // これが反映されないのは知ってる
    // copyto after clone
    Mat img_roi_copy_clone = Mat(100, 100, CV_8UC3, Scalar(255, 0, 0));
    img_roi_copy_clone.copyTo(img_roi);
    imshow("Color ROI copy after clone", frame_c);  // ナンデ!コピーできないナンデ!?

このソースコードによれば,clone()を使う方では赤い矩形が,copyTo()を使う方では先ほど描いた赤の矩形が青で塗りつぶされるため青い矩形がそれぞれ描かれるはずです.ただし我々は既にclone()が上手くいかないことを知っているので,clone()の時点ではframe_cに何も描かれず,copyTo()の方で青い矩形が初めて描かれるだろうと期待します.しかし,実際には

f:id:Mzawa2:20161208232032p:plain

このように,どちらの画像にも全く矩形が描かれなくなります.勿論,clone()ではなく=を先に書いたとしてもうまく描かれなくなります.

clone()の仕様

clone()copyTo()も,行列cv::Matの「深いコピー(deep copy)」に分類されます.じゃあclone()copyTo()の何が違うんじゃい!って思ったプログラマの皆さんなら,OpenCVソースコードを覗き込みたくなると思います.実際,cv::Mat::clone()がどんな実装なのか見てみましょう.

inline
Mat Mat::clone() const
{
    Mat m;
    copyTo(m);
    return m;
}

......

...

同じやんけ!!!

どうやら内部的にcopyTo()を呼び出しているようです(リファレンスにもそう書いてありましたが,挙動が違うことを発見したので信じられなくなって確認しました).しかしよく考えてみてください.clone()ではわざわざ自分自身をcopyTo()した画像を作って返しています.そういう意味で本当に自身のクローンを生成しているわけです.

原因の考察

ROIは通常の画像と同等に扱えるように見えるけれども,実際には元画像とデータを共有しているため,やはり普通のcv::Matとはちょっと違います.これらのことから,clone()では画像のクローンですべての情報が上書きされてしまい,元画像との参照関係が崩れてしまうのだろうと推察できます.浅いコピーの方でうまくいかない理由は,浅いコピーが実際にはデータポインタを上書きする(=元の参照関係を外す)処理であるため,これもまた元画像への参照が失われてしまうことが原因と思われます.そう考えると,一旦関係が崩れてしまったROIにいくら正しい方法で上書きしても,元画像には影響しないことが容易に察せられます.

結論

今回は,ROI処理においてclone()copyTo()およびoperator=(cv::Mat)の挙動に違いがあることを確かめました.結論としては,ROIが元画像とデータ共有している関係上,copyTo()以外を使うとその参照関係が崩れてしまい,それ以降の変更が反映されなくなるのが原因と推察できました.皆さんもROIを扱う時にはcopyTo()を使っていきましょう.