2020年7月22日 星期三

[log book] opencv 使用ROI卻無法改變ROI影像內的值

[問題描述]
在撰寫一個特別的演算法時,
需要只針對某一行做運算,因此使用cv::Rect做了一個ROI,
將某一行取出做運算,但發現我不太能修改ROI取出的影像的值。

由於程式龐大,且涉及公司機密,所以寫了一個測試用的小程式重現
#include <iostream>
#include <opencv2/opencv.hpp>
int main()
{
    cv::Mat mat = cv::Mat::ones(5, 5, CV_8UC1);
    cv::Mat mat2;
    cv::Rect ROI = cv::Rect(3, 0, 1, 5);
    std::cout << "mat = \n" << mat << std::endl;

    mat2 = mat(ROI);
    std::cout << "before change, mat2 = \n" << mat2 << std::endl;
    for (int i = 0; i < mat2.total(); i++){
        *(mat2.data + i) = 0;
    }
    std::cout << "after changed, mat2 = \n" << mat2 << std::endl;
    system("pause");
    return 1;
}

以下是執行結果:
mat =
[  1,   1,   1,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1]

test error version!
mat2 can't changed any value
before change, mat2 =
[  1;
   1;
   1;
   1;
   1]
after changed, mat2 =
[  0;
   1;
   1;
   1;
   1]
請按任意鍵繼續 . . .
ROI影像,除了第一個值被改變外,其餘的值都沒法被改變!


[解題思路]
一開始以為此錯誤的起因是影像的記憶體位置不是一列,而是一行,
所以想用reshape讓它變一列。
(我當時也覺得這種想法很荒謬,但當時完全沒有這個錯誤相關的想法,所以先假設一個「有一點點可能的」 [3] )
cv::Mat mat2 = mat.reshape(0, 1).clone();

但遇到了「The matrix is not continuous, thus its number of rows can not be changed」
裡面提到「 重新生成一个全新的矩阵,具有相同的数据,那么数据就是连续的,就能执行reshape()函数操作。」

此時,我突然想到之前讀的另一篇文章《C++ Opencv 傅里叶变换的代码实现及关键函数详解
裡面有提到copyto和clone的不同,在於會不會分配一段新的記憶體位置。
此時,我突然想到:「會不會cv::Mat(cv::Rect) 這樣的ROI,並不是複製,
而是類似於『標記、顯示在原圖的哪個位置』的作用,所以有保護機制,會將原圖的記憶體咬住,不讓別人存取(access)!?」

後來加上clone就解決了,這也表示:
如果用了 cv::Mat(cv::Rect) 這樣的ROI,由於是原圖的某個部分,所以記憶體位置是分散的,並非像重新宣告一個記憶體空間,是連續的


[解決方法]
在建使用cv::Mat(cv::Rect)時,調用其clone()方法!
因為cv::Mat(cv::Rect)和copyTo一樣是淺拷貝(shallow copy),
只會複制原cv::Mat的標頭而已
換句話說,新的cv::Mat的指標,是指向原cv::Mat的記憶體位置。
所以要搭配clone()方法,才可以達成深拷貝(deep-copy),
額外向系統要一個記憶體空間
這樣操作的時候,才不會操作到原本的cv::Mat。

修改後的程式碼如下:
#include <iostream>
#include <opencv2/opencv.hpp>
int main()
{
    cv::Mat mat = cv::Mat::ones(5, 5, CV_8UC1);
    cv::Mat mat2;
    cv::Rect ROI = cv::Rect(3, 0, 1, 5);
    std::cout << "mat = \n" << mat << std::endl;

    mat2 = mat(ROI).clone();
    std::cout << "before change, mat2 = \n" << mat2 << std::endl;
    for (int i = 0; i < mat2.total(); i++){
        *(mat2.data + i) = 0;
    }
    std::cout << "after changed, mat2 = \n" << mat2 << std::endl;
    system("pause");
    return 1;
}

執行結果
mat =
[  1,   1,   1,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1]

before change, mat2 =
[  1;
   1;
   1;
   1;
   1]
after changed, mat2 =
[  0;
   0;
   0;
   0;
   0]
請按任意鍵繼續 . . .

整理一下opencv淺拷貝(shallow copy)及深拷貝(deep copy):
  • 淺拷貝(shallow copy)
    • 雖然一樣是建一個新的cv::Mat,但成員的部分都是與舊的一樣;換句話說,影像資料是將指標指向原本的cv::Mat。
    • opencv淺拷貝的方法或函數
      • cv::Mat的方法
        • copyTo(cv::Mat)
        • cv::Mat(cv::Mat)
        • cv::Mat(cv::Rect)
        • reshape
  • 深拷貝(deep copy)
    • 建立一個全新的cv::Mat類別實體,並將原本類別的資料複制過去,影像資料也是宣告一個新的記憶體空間,並將原本影像資料複制過來。
    • opencv深拷貝的方法或函數
      • cv::Mat的方法
        • clone()

好像cv::Mat除了clone()外都是淺拷貝吔!囧



因為我想記錄一下我的思考過程,發現的原因如下(有點長,慎入!):
其實我一開始寫的小程式是這樣:
#include <iostream>
#include <opencv2/opencv.hpp>
int main()
{
    cv::Mat mat = cv::Mat::ones(3, 4, CV_8UC1);
    cv::Mat mat2;
    cv::Rect ROI = cv::Rect(1, 1, 3, 2);
    std::cout << "mat = \n" << mat << std::endl;

    std::cout << "\ntest error version!\n mat2 can\'t changed any value" <<  std::endl;
    mat2 = mat(ROI);
    std::cout << "before change, mat2 = \n" << mat2 << std::endl;
    for (int i = 0; i < mat2.total(); i++){
        *(mat2.data + i) = 0;
    }
    std::cout << "after changed, mat2 = \n" << mat2 << std::endl;
    system("pause");
    return 1;
}

執行結果:
mat =
[  1,   1,   1,   1;
   1,   1,   1,   1;
   1,   1,   1,   1]

test error version!
mat2 can't changed any value
before change, mat2 =
[  1,   1,   1;
   1,   1,   1]
after changed, mat2 =
[  0,   0,   0;
   0,   0,   1]
請按任意鍵繼續 . . .

才發現,其實也不是完全不能改變其值,
但的確會有問題!
由於一開始是使用vim + mingw + cmake去編譯的,
我還以為是編譯器的問題(因為算法的專案用是使用visual studio 2015),
結果用VS也寫一個,發現結果一樣,才確定不是編譯器的問題!

剛解決完時,我還是沒想到為什麼要加clone。
後來我突然想到一件事:
cv::Mat(cv::Rect)是不是像是copyTo方法一樣,
只是把標頭給新的cv::Mat而已,所以我就把程式改回錯的版本,
並在修改mat2後,把mat顯示出來,果然驗證了我的想法!
程式如下:
#include <iostream>
#include <opencv2/opencv.hpp>


int main()
{
    cv::Mat mat = cv::Mat::ones(5, 5, CV_8UC1);
    cv::Mat mat2;
    cv::Rect ROI = cv::Rect(3, 0, 1, 5);
    std::cout << "mat = \n" << mat << std::endl;


    //mat2 = mat(ROI).clone();
    mat2 = mat(ROI);
    std::cout << "before change, mat2 = \n" << mat2 << std::endl;
    for (int i = 0; i < mat2.total(); i++){
        *(mat2.data + i) = 0;
    }
    std::cout << "after changed, mat2 = \n" << mat2 << std::endl;
    std::cout << "mat = \n" << mat << std::endl;


    system("pause");
    return 1;
}

執行結果如下:
mat =
[  1,   1,   1,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1]
before change, mat2 =
[  1;
   1;
   1;
   1;
   1]
after changed, mat2 =
[  0;
   1;
   1;
   1;
   1]
mat =
[  1,   1,   1,   0,   0;
   0,   0,   0,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1;
   1,   1,   1,   1,   1]
請按任意鍵繼續 . . .

所以要搭配clone()方法,才可以達成深拷貝(deep-copy),
額外向系統要一個記憶體空間,這樣操作的時候,才不會操作到原本的cv::Mat
後來我針對我這個想法,找了一些相關資料:[4]~[6]


參考資料:




這封郵件來自 Evernote。Evernote 是您專屬的工作空間,免費下載 Evernote

沒有留言:

張貼留言