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

此为OpenCV官方教程的个人翻译

目标

这篇文章将为您讲解:

  • 如何遍历图像的每一个像素?
  • OpenCV中的矩阵如何存储?
  • 如何衡量我们使用算法的性能?
  • 什么是查找表?为什么我们要使用它?

测试用例

​ 考虑一个简单的减色方法,使用C和C++里的unsigned char类型来作为矩阵的数据类型.因此该矩阵中,每一像素通道都可能含有多达256种不同的数值.对于一个三通道图像,这会造成非常非常多组合(大概1600万).直接操作这些色彩可能会对我们算法的性能造成极大影响.然而,有时可能操作更少数量的色彩,却达到相同的结果.

​ 在这种情况下,我们通常会进行色彩空间缩减。这意味着我们将颜色空间当前值与新的输入值进行划分,最终得到更少的颜色。例如,介于 0 和 9 之间的每个值都采用新值 0,10 到 19 之间的每个值都取值 10,依此类推。

​ 当您将 uchar(无符号 char - 即值介于 0 和 255 之间)值与 int 值相除时,结果也将是 char。这些值只能是字符值。因此,任何分数都将向下舍入。利用这一事实,uchar域中的上部运算可以表示为:

\[I_{new}=\frac{I_{old}}{10}*10 \]

​ 一个简单的色彩空间减少算法将仅传递图像矩阵的每个像素并应用此公式。值得注意的是,我们做了一个除法和乘法运算。对于系统来shu说,这些操作开销非常大。如果可能的话,值得通过使用开销更低的操作来避免它们,例如一些减法,加法在最好情况下就是简单赋值。此外,请注意,对于上述运算,我们只有有限数量的输入值。在使用uchar的情况下,该值为256.

​ 因此,对于较大的图像,明智的做法是事先计算所有可能的值,并且在分配期间只需使用查找表进行分配即可。查找表是简单的数组(具有一个或多个维度),对于给定的输入值变体,它保存最终的输出值。它的优势在于我们不需要进行计算,我们只需要读取结果即可。

​ 我们的测试用例程序(以及此处提供的示例)将执行以下操作:读取控制台行参数所提供的图像,并使用给定的控制台行参数整数值应用缩减。在OpenCV中,目前它们是逐个像素地浏览图像的三种主要方式。为了使事情变得更有趣,将使用所有这些方法扫描每个图像,并打印出花费的时间。

​ 最后一个参数是可选的。给定图像将以灰度格式加载,否则使用BGR颜色方式。第一件事是计算查找表。

    string input;
    cin >> input;
    stringstream s;
    s << input;
    int divideWith;
    s >> divideWith;
    if (!s || !divideWith) {
        cout << "Invalid number entered for dividing" << endl;
        return -1;
    }
    uchar table[256];
    for (int i = 0; i < 256; i++) {
        table[i] = (uchar)(divideWith * (i / divideWith));
    }

​ 在这里,我们首先使用 C++ stringstream 类将输入字符串从文本转换为整数格式。然后,我们使用简单的外观和上面的公式来计算查找表。这里没有OpenCV特定的东西。

​ 另一个问题是我们如何测量时间?OpenCV提供了两个简单的函数来实现这个getTickCount()getTickFrequency()第一个返回来自特定事件(例如,自您启动系统以来)的系统 CPU tick 。第二个返回 CPU 在一秒内发出tick的次数。因此,以秒为单位测量两次操作之间经过的时间数很容易.

   double t = (double)getTickCount();
   // do somthing
   t = ((double)getTickCount() - t) / getTickFrequency();
   cout << "Times passed in seconds: " << t << endl;

图像矩阵如何保存在内存中?

​ 矩阵的大小取决于所使用的颜色系统。更准确地说,这取决于所使用的通道数量。对于灰度图像,我们有如下内容:

1.png

​ 对于多通道图像,列包含的子列数与通道数一样多。例如,在BGR颜色系统的情况下:

2.png

​ 因为在许多情况下,内存足够大,可以连续地存储行,因此行可能会一个接一个存储,从而创建一个长行。由于所有内容都是连续的,因此可以一个接一个地计算,这可能有助于加快扫描过程。我们可以使用 isContinuous()函数来确认矩阵是否属于这种情况。

更高效的方式

​ 在性能方面,最快的便是经典的C样式运算符[](指针)访问。因此,我们可以推荐的最高效的进行分配的方法是:

Mat& ScanImageReduceC(Mat& I, uchar* table) {
    // accept only char type matrIces
    CV_Assert(I.depth() == CV_8U);

    int channels = I.channels();

    int nRows = I.rows;
    int nCols = I.cols;

    if (I.isContinuous()) {
        nCols = nRows * nCols * channels;
        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] = table[p[j]];
        }
    }
    return I;
};

​ 在这里,我们基本上只是获取一个指向每行开头的指针,并遍历它直到结束。在矩阵以连续方式存储的特殊情况下,我们只需要请求指针一次,然后一直走到最后。我们需要注意彩色图像:我们有三个通道,因此我们需要在每行中传递三倍以上的项。

​ 还有另一种方式。Mat 对象成员变量data返回指向第一行(第一列)的指针。如果此指针为 null,则该对象中没有有效的输入。这是检查图像加载是否成功的最简单方法。如果存储连续,我们可以使用它来遍历整个数据指针。如果是输入灰度图像,则可以按下面的进行操作:

uchar* p = I.data;

for( unsigned int i =0; i < ncol*nrows; ++i)
    *p++ = table[*p];

​ 你会得到同样的结果。但是,此代码以后更难阅读。如果你有一些更先进的技术,那就更难了。此外,在实践中,这会获得相同的性能结果(因为大多数现代编译器可能会自动为您进行这个小的优化)。

迭代器方式(安全方式)

​ 如果您以指针以及[]方式进行遍历,那么正确处理可能出现的内存间隙就是您的责任.迭代器方法被认为是一种更安全的方式,因为它从用户那里接管这些任务。您需要做的就是询问图像矩阵的开始和结束,然后只需增加开始迭代器,直到到达终点。要获取迭代器指向的值,请使用 * 运算符。

Mat& ScanImageAndReduceIterator(Mat& I, uchar* table) {
    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]];
            }
        }
        default:
            break;
    }
    return I;
}

​ 对于彩色图像,每列有三个uchar子列。这可以被认为是uchar项目的简短向量,在OpenCV中以Vec3b命名。要访问第 n 个子列,我们使用简单的运算符 [] 访问。请务必记住,OpenCV 迭代器会遍历各列并自动跳到下一行。因此,对于彩色图像,如果您使用简单的uchar迭代器,您将只能访问蓝色通道值。

使用引用进行动态地址计算

​ 不建议使用此方法进行遍历。它是为了获取或修改图像中的随机元素而制作的。其基本用法是指定要访问的项目的行号和列号。在我们早期的遍历方法中,您已经可以通过我们查看图像的类型来观察到这一点很重要。此外,您需要手动指定在自动查找时使用的类型。对于以下源代码的灰度图像(一并演示了at()函数的用法),您可以观察到这一点:

Mat& ScanImageAndReduceRandomAccess(Mat& I, uchar* table)
{
    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_<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;
    }
    default:
        break;
    }
    return I;
}

​ 这些函数采用您指定输入类型和坐标,并动态计算查询项的地址。然后返回对此的引用。当您获取值时,这可能是一个常量,当您设置值时,这可能是一个非常量。在debug模式下,会有以安全检查措施:将对输入坐标是否有效且存在进行检查。如果不存在或者无效,您将在标准错误输出流上收到一个很好的输出消息。与release模式下的C风格[]方式相比,使用此方法的唯一区别是,对于图像的每个元素,您将获得一个新的行指针,用于我们使用C运算符[]来获取列元素的内容。

​ 如果需要使用此方法对图像进行多次查找,则为每个访问输入类型和 at 关键字可能会很麻烦且耗时。为了解决这个问题,OpenCV具有Mat_数据类型。它与Mat相同,在定义时,您需要通过查看数据矩阵的内容来指定数据类型,但是作为回报,您可以使用operator()来快速访问项目。]同时,OpenCV也提供了很方便MatMat_的转化方式(如上示例)。尽管如此,重要的是要注意,相同的操作(具有相同的运行时速度)可以使用at()函数完成。

主要函数

​ 这是在图像中实现查找表修改的额外方法。因为在图像处理中,将所有给定的图像值替换为其他值是很常见的,故OpenCV提供了一个函数,该函数无需您自己编写遍历方法便可以进行转变。我们使用核心模块的LUT()功能。首先,我们构建查找表的 Mat 类型:

    Mat lookUpTable(1, 256, CV_8U);
    uchar* p = lookUpTable.data;
    for (int i = 0; i < 256; i++)
    {
        p[i] = table[i];
    }

最后调用函数(I 是我们的输入图像,J 是输出图像):

    LUT(I, lookUpTable, I);

上述方法的性能差异

方法 耗时
迭 代 83.7201 毫秒
实时 RA 93.7878 毫秒
LUT 函数 32.5759 毫秒
C风格[] 79.4717 毫秒

​ 如果可能的话,使用OpenCV已经提供的函数(而不是重新造轮子)。最快的方法是LUT函数。这是因为 OpenCV 库是通过英特尔线程构建模块启用多线程的。但是,如果您需要编写简单的图像扫描,则首选指针方法。迭代器是一个更安全的方法,但速度相当慢。在调试模式下,使用动态引用访问方法进行全图像扫描是最慢的的。在发布模式下,它可能会快于迭代器方法,但肯定会为此牺牲迭代器的安全特性。

posted @ 2022-06-26 09:39  帝皇の惊  阅读(37)  评论(0)    收藏  举报