OpenCV学习笔记

2020/02/02 OpenCV C++ Image Library

OpenCV的学习笔记整理,包括的都是一些常见用法,例如Mat的遍历等。


cv::Mat Usage 使用说明

参考:Mat - The Basic Image Container

Mat构成

cv::Mat的构成很特殊。它包含两个部分:

  • 矩阵头部(matrix header),它包含了诸如矩阵的尺寸和存储方式等。因此,Mat的header的大小通常是固定的,而整个Mat大小显然不固定,因为矩阵主体数据不固定。
  • 一个指向矩阵主体数据的指针。也就是说,两个不同的cv::Mat的指针可以指向同一块数据区域。这样的好处是,图片通常都比较大,这样无需分配额外内存来存储多余的图片。当然缺点很明显,如果Mat指向的数据区域被release的话,所有的指向它的Mat都会失效。这样的话,将来再次尝试获取某个像素的话就会出错。

因此,Mat的Copy constructor和Assignment operator都是仅仅拷贝了矩阵的header,并未拷贝矩阵主体。换句话说,如果原始的Mat被release的话,新的拷贝Mat所指向的矩阵主体就会为空。同样的,如果修改拷贝的Mat,那么原始的Mat指向的矩阵主体也会改变。

// 此时只是定义了矩阵的header部分,还未给矩阵主体分配空间
cv::Mat A, C; 
// 读入图片同时为矩阵主体分配内存空间
A = cv::imread(argv[1], CV_LOAD_IMAGE_COLOR); 
// 注意:这里的B只拷贝了A的header而已,它的指针指向A指向的矩阵主体部分。
// 此后如果修改B的话,A指向的内容也会被改变,这很危险。
cv::Mat B(A);
// Assignment operator和上面的Copy Constructor作用相同,还是只拷贝了header部分。
C = A; 

那么,如果真的想要整体拷贝一个Mat全部内容怎么办?有两个函数:clone()以及copyTo():

Mat F = A.clone(); // 直接硬拷贝
Mat G;
A.copyTo(G); // copyTo和clone()结果完全一样,就是用法稍有不同

初始化

参考:

这里简单放几个常用的:

// 初始化元素完全相同
Mat M(2,2, CV_8UC3, Scalar(0,0,255)); // 最后一个参数是初始化的constant数值
// 初始化元素不相同
cv::Mat T1 = (cv::Mat_<double> (3,4) <<
    1,0,0,0,
    0,1,0,0,
    0,0,1,0); // cv::Mat_是最底层的Mat类,需要重载元素类型。注意这种初始化方式是行优先的(row-major)

Mat成员函数

例子:

if (img.depth() == CV_8U){
    // depth()函数获取的是深度,即每个像素点中每个元素的类型。这里就是8-bit unsigned char类型
}
if (img.type() == CV_8UC3){
    // type()函数获取的是详细类型,这里就是8-bit unsigned char且有3个channels
}
if (img.channels() == 3){
    // channels()获取的是每个像素点的元素个数,例如如果是BGR图片,这个值就是3
}

Mat的类型

Mat的类型可以通过成员函数type()获取,但是它输出的是一个整数而不是可读性好的类似8UC1这种。例如,0就对应8UC1, 18是8UC3等。并且,OpenCV中居然没有将前者转换成后者的函数。下面给出这个函数,并且注释中有输出的整数和类型的对照表。

//! Convert the type of a cv::Mat object to a readable string
/*
* Correspondence between type number and string is in this table:
*
+--------+----+----+----+----+------+------+------+------+
|        | 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 |
+--------+----+----+----+----+------+------+------+------+
*/
std::string getCvMatType(int type)
{
    std::string r;

    uchar depth = type & CV_MAT_DEPTH_MASK;
    uchar chans = 1 + (type >> CV_CN_SHIFT);

    switch (depth)
    {
        case CV_8U:
            r = "8U";
            break;
        case CV_8S:
            r = "8S";
            break;
        case CV_16U:
            r = "16U";
            break;
        case CV_16S:
            r = "16S";
            break;
        case CV_32S:
            r = "32S";
            break;
        case CV_32F:
            r = "32F";
            break;
        case CV_64F:
            r = "64F";
            break;
        default:
            r = "User";
            break;
    }
    r += "C";
    r += (chans + '0');
    return r;
}

其它使用方法

// 获取一个Mat中的一小块矩形部分
cv::Mat small_img = img(cv::Rect(0, 0, 100, 200)); // 注意这里依然是只拷贝了header
// 修改这个小块。注意此时img指向的矩阵主体也会改变,因为它们指向同一个主体部分
small_img.setTo(0);

Access an image 操作图片像素

参考:How to scan images, lookup tables and time measurement with OpenCV

OpenCV中,一张图片中每个像素点可能只有一个unsigned char变量,即8-bit元素,例如灰度图片;也可能是多个unsigned char变量,例如RGB或者RGBA彩色图片。这里每个像素中有几个量就叫做通道(channels)。灰度图片(grayscale)就是单通道,RGB图片就是三通道。下图演示了RGB图片中的数据存储:

OpenCV_image_channels

注意

  • OpenCV中读入彩色图片到cv::Mat中后,默认是按照BGR顺序,即蓝绿红,如上图所示。而通常很多图片存储后是按照RGB顺序的。这点一定要注意。
  • 可以通过cv::Mat类的成员函数channels()获取该图片的通道个数。

Get one pixel 获取一个像素

如果只是想要获取某一个像素的话,可以使用OpenCV中为Mat定义的at()函数:

// 灰度图像每个像素点就是一个unsigne char
unsigned char gray = gray_img.at<uchar>(i,j); 
// 彩色图像每个像素点包含RGB这三个channels(有时候是RGBA四个)
unsigned char b = color_img.at<cv::Vec3b>(i,j)[0]; 
unsigned char g = color_img.at<cv::Vec3b>(i,j)[1];
unsigned char r = color_img.at<cv::Vec3b>(i,j)[2];

Scan entire image 遍历整张图片

1. The efficient way 最快的方法:基于C指针

需要注意的是,虽然一张图片的物理尺寸是width x height,但是实际上,由于内存通常都足够大,因此OpenCV中的cv::Mat读入图片后,其实通常是将图片中全部的行按照顺序存储到一行中的。即,类似于将所有的行“平铺”到一行,这样的话,一张图片其实就是一个一维数组。这样做的好处是,可以加快扫描整张图片的速度。当然,这只是底层的存储方式有变化而已,该图片的顶层的各种参数全都不变。当然这也不一定,因此在实现中,可以用isContinuous()函数来判断一个cv::Mat变量是否是连续存储的。

如果是只存储一行,那么最快的获取每个像素的方法就是使用C中的操作符[]。下面代码的作用是,遍历整张图片,并将每个像素的灰度值通过table来in-place得映射到另一个值。

// 遍历整张图片,并将每个像素的灰度值通过table来in-place得映射到另一个值
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);
    int channels = I.channels();
    int nRows = I.rows;
    int nCols = I.cols * channels;
    // 检查一下是否是连续存储的。如果是,则更新行和列的值。
    if (I.isContinuous())
    {
        nCols *= nRows;
        nRows = 1;
    }
    int i,j;
    uchar* p;
    for( i = 0; i < nRows; ++i)
    {
        p = I.ptr<uchar>(i); // 先获取每一行开头的指针
        for ( j = 0; j < nCols; ++j)
        {
            // p[j]就是每个像素了。
            // Here it maps each pixel's grayscale value to another value.
            p[j] = table[p[j]];
        }
    }
    return I;
}

上面代码调用了isContinuous()来判断是否为连续存储的(即一行)。

2. The iterator (safe) way 迭代器做法(更安全)

上面的基于C的快速法中,用户需要自行确保判断了全部的channels、跳过有可能出现的行和行之间的gaps,并且确保不能越界。相比之下,使用C++的iterator更加安全一些。只需获取Mat的起始和终止指针,在中间遍历即可。不过,该方法还是比上面的C指针的方法慢一些。下面代码作用和上面的代码相同,只是增加了对BGR彩色图片的处理。

// 依然是遍历整张图片,并将每个像素的每个通道的颜色值通过table来in-place得映射到另一个值
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);
    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;
}

3. On-the-fly address calculation 即时计算每个像素的地址

另一种方法是,我们直接使用前面讲述的获取每个像素的函数at(),获取每个像素点的位置指针就行了。但是,这种方法通常不推荐,因为它通常用于随机获取任意位置的像素。

Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);
    const int channels = I.channels();
    switch(channels)
    {
    case 1:
        {
            for( int i = 0; i < I.rows; ++i)
                for( int j = 0; j < I.cols; ++j )
                    I.at<uchar>(i,j) = table[I.at<uchar>(i,j)]; // 和前一种方法唯一的不同之处
            break;
        }
    case 3:
        {
        // 下面使用了Mat_类型,它是一种Mat的指针类型。使用它的好处是,在循环中就不用每次都要显式的像I.at<Vec3b>这样指定类型了。当然它的速度其实是和直接使用.at<Vec3b>一模一样,因此它的好处只是让代码更简洁一点而已。
         Mat_<Vec3b> _I = I;
         for( int i = 0; i < I.rows; ++i)
            for( int j = 0; j < I.cols; ++j )
               {
                   _I(i,j)[0] = table[_I(i,j)[0]];
                   _I(i,j)[1] = table[_I(i,j)[1]];
                   _I(i,j)[2] = table[_I(i,j)[2]];
            }
         I = _I;
         break;
        }
    }

    return I;
}

4. The Core Function 直接使用OpenCV相关函数

OpenCV定义了大量的和图片处理相关的函数供使用。因此,如果满足需求的话,可以直接使用这些函数,而不用再遍历整张图片每个像素了。例如,上面的例子所做的事情都是遍历整张图片,并将每个像素的每个通道的颜色值通过table来in-place得映射到另一个值。即,输入的table充当的是look-up table的作用。而OpenCV中已经定义了一个LUT()的函数来做这件事情(参见:https://docs.opencv.org/2.4/modules/core/doc/operations_on_arrays.html#lut。实验结果表明,直接使用LUT()是本章这些方法中最快的。这是因为,OpenCV中定义的函数通常使用了基于Intel TBB(Threaded Building Blocks)的多线程以加速。

Mat& ScanImageAndReduceCoreFunction(Mat& I, const uchar* const table)
{
    // 定义look-up table变量是一个1x256的矩阵
    Mat lookUpTable(1, 256, CV_8U);
    uchar* p = lookUpTable.data;
    for( int i = 0; i < 256; ++i)
        p[i] = table[i]; // 因为lookUpTable只有一行,因此直接赋值即可
    cv::Mat J = I.clone();
    cv::LUT(I, lookUpTable, J); // 调用已定义的函数,J是输出
    return J;
}

Summary 用法总结

在实现中,如果你的需求可以直接使用OpenCV的函数实现,那么直接使用它即可。显然它是最快并且基本上是最安全的方法。否则,尽可能使用上面第1中方法,即基于C指针的快速法。

一些常见的函数

作为函数参数的 Mask

很多 OpenCV 的函数都会有一个称为 Mask 的 Mat 矩阵作为输入或者输出。它的 size 通常是和作为主体输入或输出的 Mat 相同(例如一张图片),而每个元素的 type 通常是 unsigned char,即它通常类型是 8UC1。而它的元素值通常只是 binary 的,即, 0 表示对应的主体 Mat 的点没有被选中,1 表示被选中了。

cv::Mat mask;
cv::Mat E = cv::findEssentialMat(selected_points1, selected_points2, Kd.at<double>(0,0), cv::Point2I d(image1.cols/2., image1.rows/2.),
                                 cv::RANSAC, 0.999, 1.0, mask);
vector<cv::Point2f> inlier_match_points1, inlier_match_points2;
for(int i = 0; i < mask.rows; i++) {
    if(mask.at<unsigned char>(i)){
        // 只对选中的特征点进行操作
        inlier_match_points1.push_back(selected_points1[i]);
        inlier_match_points2.push_back(selected_points2[i]);
    }
}

常见的 cv::Mat 的操作

几乎所有常见的 Mat 操作都在这里了:Operations on Arrays。其中,常见的有:

  • 两个 Mat 之间的加减乘除
  • 和另一个 Mat 的位操作,例如与或非;
  • 一些特殊的计算,例如指数对数;
  • 特征值特征向量;
  • 求和、平方和、最大值、最小值、方差、均值、直方图等各种统计;

不过注意,上面这个文档是 OpenCV 2.4 的,最新的 OpenCV 4 中的函数定义可能会稍有不同,不过函数名称几乎都不变。例如,个人记得是,OpenCV 2 中,如果除数是 0,那么最终除法的结果也是 0(即,避开了被除数是 0 的情况)。而 OpenCV 4 中,如果被除数是 0,那么最终除法的结果是 NaN 之类的值(需要额外增加一个判断),无论除数是否为 0。

Search

    Table of Contents