1. 程式人生 > >那些年在Opencv遇到過的Mat坑

那些年在Opencv遇到過的Mat坑

本文記錄一些遇到過的Mat坑,以及易淆的知識點。

1.熱身:Mat成員之易淆:

a. Mat.depth()

depth()得到的是一個0~6的數字,分別代表單個圖不同的深度,對應關係如下:

+--------+----+----+----+----+------+------+------+------+
|        | C1 | C2 | C3 | C4 | C(5) | C(6) | C(7) | C(8) |
+--------+----+----+----+----+------+------+------+------+
| CV_8U  |  0 |  8 | 16 | 24 |   32 |   40 |   48 |   56 |
| CV_
8S | 1 | 9 | 17 | 25 | 33 | 41 | 49 | 57 | | CV_16U | 2 | 10 | 18 | 26 | 34 | 42 | 50 | 58 | | CV_16S | 3 | 11 | 19 | 27 | 35 | 43 | 51 | 59 | | CV_32S | 4 | 12 | 20 | 28 | 36 | 44 | 52 | 60 | | CV_32F | 5 | 13 | 21 | 29 | 37 | 45 | 53 | 61 | | CV_64F | 6 | 14 | 22 | 30 | 38 | 46 | 54 | 62 | +--------+----+----+----+----+------+------+------+------+

其中:

CV_8U   A.at <unsigned char>
CV_8S   A.at <char>
CV_16S  A.at <short>
CV_16U  A.at <unsigned short>
CV_32S  A.at<int>
CV_32F  A.at<float>(0,1)
CV_64F  A.at<double>

不可用A.at(0,0)來取深度CV_64F的矩陣值,會報錯
**note:
之前寫findFundamental時,呼叫opencv 的庫,計算極線誤差總是不對,最終才發現cv::findFundamentalMat返回的是CV_64F的矩陣,但我採用F.at(1,0)來獲取其中的值,結果無窮大,所以得到的結果是錯的。**
可以通過type()來獲取矩陣型別(對應上面0-6)。
用channels()來獲取通道數,如CV_32FC1的通道數為1,則返回1.

convertTo 只能轉換type,不能轉換通道
也就是如果一個CV_U8C1的image;
image.convertTo(newImage, CV_U8C3);
那麼newImage還是CV_U8C1(CV_U8C1和CV_U8C3)
將1通道的轉換成3通道的用:
cvtColor(src,dst,CV_GRAY2RGB);
將彩色圖轉換成灰度圖用:
cvtColor(src,dst,CV_RGB2GRAY);
cvtColor用於在彩色、灰度、HSV、YCrCb等色彩空間相互轉換

b.矩陣行列數目

列印 matA.size()

[3,4]

matA行為4,列為3
可以通過matA.size().width,matA.size().height得到正確行列數

c.其他常見常用函式
1. clone() 建立一個影象的深拷貝
Mat image;
image = cv::imread("boldt.jpg");
Mat cloneimage = image.clone();
2. create()函式 
result.create(image.rows,image.cols,image.type()); 
create函式建立的影象的記憶體是連續的,不會對影象的行進行填補,分配的記憶體大小為total()*elemSize()

3. total()函式 
返回Mat矩陣的畫素個數

4. elemSize()函式 
返回Mat矩陣每個畫素的位元組數

5. isContinuous() 
判斷Mat所表示的那副影象是否連續,即是否進行了行填補;如果返回為真的話,沒有進行行填補,反之就進行了行填補

6. data成員變數 
data是一個unsigned char的指標,代表Mat記憶體的首地址

uchar *data = image.data;

7.step成員變數 
step代表Mat矩陣的行款,包括填補畫素

8.setTo函式 
設定矩陣的值
image.row(0).setTo(cv::Scalar(0))或 image.row(0).setTo(cv::Scalar(0,0,0));

2.矩陣之間運算

A*B;//普通矩陣乘法

矩陣乘法需要注意的是,矩陣之間的型別必須一致,否則會報錯:

OpenCV Error: Assertion failed (type == B.type() && (type == CV_32FC1 || type == CV_64FC1 || type == CV_32FC2 || type == CV_64FC2)) in gemm, file /home/kevin/DevLib/opencv-3.2.0/modules/core/src/matmul.cpp, line 1530

需要使用A.covertTo(A,CV_32F)等方式來使得AB兩者的型別一致。
CV_32F等價於CV_32FC1
同時需要注意,opencv構造基礎矩陣等函式預設形成CV_64FC1的型別。
A.dot(B)//叉積:先將矩陣轉換成一個向量,再將向量vA.dot(vB);得到一個浮點數

A.mul(B)//點乘./,得到矩陣
做矩陣乘法時,除了行列要滿足相應要求外,資料的位數(可以通過convertTo轉換)要一致

3.影象之間進行運算

opencv 影象與一般矩陣不同,額外含有通道等資訊,因此不能對影象矩陣直接使用 *,+等,需要如下操作:

void add(InputArray src1, InputArray src2, OutputArray dst,
                  InputArray mask=noArray(), int dtype=-1);

void subtract(InputArray src1, InputArray src2, OutputArray dst,
                       InputArray mask=noArray(), int dtype=-1);

void multiply(InputArray src1, InputArray src2,
                       OutputArray dst, double scale=1, int dtype=-1);

void divide(InputArray src1, InputArray src2, OutputArray dst,
                     double scale=1, int dtype=-1);

void divide(double scale, InputArray src2,
                     OutputArray dst, int dtype=-1);

void scaleAdd(InputArray src1, double alpha, InputArray src2, OutputArray dst);

void addWeighted(InputArray src1, double alpha, InputArray src2,
                          double beta, double gamma, OutputArray dst, int dtype=-1);

4.opencv的mat運算效率

Mat運算速度較快,不知其加速機制,但測試發現同為影象相加,c風格陣列相加,時間是opencv的影象相加的10倍。


#include <opencv2/opencv.hpp>//the total header
using namespace std;
using namespace cv;

class Timer{
public:
    Timer(){}
    void Start(){
        t1 = std::chrono::steady_clock::now();
    };
    void End(){
        t2 = std::chrono::steady_clock::now();

        std::cout<<"Time used: "<<std::chrono::duration_cast<std::chrono::microseconds>(t2-t1).count()<<" microseconds."<<std::endl;
    }
private:
    std::chrono::steady_clock::time_point t1, t2;
};

int main(){
    cv::Mat img2 = imread("2.png");
    cv::Mat img1 = cv::imread("1.png");
    cvtColor(img1, img1, COLOR_BGR2GRAY);
    cvtColor(img2, img2, COLOR_BGR2GRAY);
    std::cout<<img1.depth()<<" "<<img1.channels()<<std::endl;
    Timer timer;
    //img1.resize(480,480);
     int rows = img1.rows, cols = img1.cols;
    int length = rows * cols;
    uchar * uarray =  new uchar[length];

    for(int i = 0; i < img1.rows; i++)
        for(int j = 0; j < img1.cols; j++)
            uarray[i*img1.cols+j] = img1.at<uchar>(i,j);

    timer.Start();
    multiply(img1,img2,img1);
    timer.End();

    timer.Start();
    for(int i = 0; i< length; i++)
        uarray [i] = uarray[i]+uarray[i];
    timer.End();
    return 0;

6.獲取/設定影象畫素值

不考慮速度的話,用方法一即可,如果涉及到大量/重複獲取全部影象的全部畫素,則可以考慮方法二
方式一:動態獲取:.at
比如對於4x1的矩陣,即使可以寫作.at(3,1),也可以寫作.at(3)
寫在例程前面:獲取矩陣值必須十分小心*

一是.at<>(i,j)取值不會檢查i,j的值,即使i,j已經出了邊界,也不會報錯,只是按照記憶體取其他區域的值。
對此opencv文件寫得十分清楚:For the sake of higher performance, the index range checks are only performed in the Debug configuration.
此處不知它的debug configuration是我自己工程的設定,而實際使用中為了速度都是使用release編譯的opencv版本。而根據自己測試,在CmakeLists設定關閉CMAKE_CXX_FLAGS並開啟debug模式,還是不會啟動它的index檢查。因此使用.at時要特別注意
二是如本文開頭提到的,模板中的型別精確匹配十分重要。

Vec3b intensity = img.at<Vec3b>(y, x);
Vec3b intensity = img.at<Vec3b>(Point2(x,y))
uchar blue = intensity.val[0];
uchar green = intensity.val[1];
uchar red = intensity.val[2];
//設定畫素
img.at<float>(320,240) = 255;
const int channels = I.channels();  
    switch(channels)  
    {  
    case 1:  
        {  
            MatIterator_<uchar> it, end;  
            for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)  
                *it = table[*it];  
            break;  
        }  
    case 3:  
        {  
            MatIterator_<Vec3b> it, end;  
            for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)  
            {  
                (*it)[0] = table[(*it)[0]];  
                (*it)[1] = table[(*it)[1]];  
                (*it)[2] = table[(*it)[2]];  
            }  
        }  
    }  
    return I;  

7.點序列轉換成影象矩陣

std::vector<cv::Point2f>points
cv::Mat img(points);//img的行列數目是N,2   (32FC1)

std::vector<Point3f> points;
Mat pointsMat = Mat(points).reshape(1);//pointsMat:N x 3   (32FC1)

8.Mat中符號過載:

operator = :是指傳遞引用,非值傳遞,值傳遞需要用A.copyTo(B)。這與標準庫的vector等容器對 = 的過載表示硬拷貝(值傳遞)不一樣

附錄 c++知識:
當涉及以物件為函式實參時,以下情形會呼叫拷貝建構函式:
注意拷貝建構函式有:Class A(){};Class A(const Class& a); Class A (Class& a);
a.當將物件作為實參傳入函式時,如果是值傳遞,則會呼叫類的拷貝建構函式,當函式結束時,呼叫解構函式.
b.當g_Fun()函式執行到return時,會產生以下幾個重要步驟:
(1). 先會產生一個臨時變數,temp。
(2). 然後呼叫拷貝建構函式把retVal的值給temp。這兩個步驟有點像:Cclass temp(retVal);
(3). 在函式執行到最後先析構retVal區域性變數。
(4). 等g_Fun()執行完後再析構temp物件。

當呼叫拷貝建構函式時,如果沒有重寫建構函式,則預設會進行深度拷貝成員變數(靜態成員除外);
若重寫了拷貝建構函式,拷貝建構函式將會在初始化形參時被呼叫,不再直接進行記憶體拷貝工作。

當傳入引數為引用時,不會呼叫建構函式。
當返回值為引用時,不會呼叫建構函式,也不會呼叫解構函式。

9.再談Mat的淺拷貝由於Mat拷貝建構函式Mat(const Mat &m)沒有拷貝記憶體,只是將新的物件的指標指向m的記憶體區(淺拷貝),所以初始化和返回時都應該用.clone()確保進行了深度拷貝

Mat 的各種運算,包括加減乘除、.t(),.inv()等都會創造一個臨時結果變數,但仍然存在大量的成員函式返回的只是一個Mat頭指標比如A.rowrange(0,3)。因此初次使用時需要注意甄別,對後者使用clone()深度拷貝一份。

正因如此,對於Mat 的 vector,push_back在執行拷貝構造時也未拷貝記憶體,因此存在:

Mat m(1,1);
vector<Mat>vecMatrix;
for(int i = 0; i < 10; i++){
    m.at<float>(0) = float(i);
    vecMatrix.push_back(m);
}

這樣處理後會發現vecMatrix所有成員的資料都是9。
因此當push_back()時,一定要push_back新建立的Mat或使用深度拷貝後的mat:

Mat m(1,1);
vector<Mat>vecMatrix;
for(int i = 0; i < N; i++){
    m.at<float>(0) = float(i);
    vecMatrix.push_back(m.clone());
}

10.其他

經Berwin1973同學評論:
opencv2.x時候,有直接從IPLImage結構轉為Mat影象結構的建構函式 Mat(IplImage*),而在3.x中此建構函式被移除

最後

這是之前寫的部落格,最近評論時重看了下,感覺有經常遇到的問題,也可以抽空去opencv下邊issue下,助力opencv社群成長。
anyway,我用OpenCV,我快樂:)