opencvsharp遍历图像像素 opencv遍历像素的方式_opencv 效率


在开发图像处理项目时,会遇到访问图像的每个像素的情况。本节主要内容是OpenCV如何访问像素,怎样提高效率,如何评价算法的性能。

目标

  • 遍历图像的每个像素
  • 内存中矩阵数据的存储
  • 测量算法性能
  • lookup table是什么
  • 原文网址How to scan images, lookup tables and time measurement with OpenCV
  • 本地目录D:opencvsourcesdoctutorialscorehow_to_scan_images
  • 代码目录D:opencvsourcessamplescpptutorial_codecorehow_to_scan_images
  • GitHub 有相应文档和OpenCV源代码
  • 版本OpenCV4.1.2(版本兼容性见英文原文,部分文档适用于OpenCV2.0和3.0)
  • 环境Windows、C++、VS2019 Community

测试案例 Our test case

一个颜色量化的例子。对于RGB图像的一个像素,有三个通道,每个通道的数据是unsigned char,那么组合起来就是1600万中颜色。在某些应用场景中这么多颜色会严重影响算法性能,并且去掉大多数颜色不会对最终结果造成影响。这种方法称为颜色空间退化,也就是把某些颜色用同一种颜色代替。比如数值0~9用0代替,10~19用10代替。对于unsigned char(0~255),除以int型,结果类型不变,会向下圆整,用下面公式可实现。



对于大型图像,每个像素进行一次除法和乘法,执行起来效率很低。如果对于给定的像素值,不再计算,而是查找对应的量化值,那么效率就会提升。这里就用到了lookup table。下面是创建lookup table的方法。


int divideWith = 10; //量化分辨率
    uchar table[256];
    for (int i = 0; i < 256; ++i)
       table[i] = (uchar)(divideWith * (i/divideWith));


那么如何测量执行时间,OpenCV提供了下面的方法。


double t = (double)getTickCount();
// 待测试的代码
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "运行时间(秒): " << t << endl;


图像矩阵在内存中如何存储 How is the image matrix stored in memory?

对于灰度图像:


opencvsharp遍历图像像素 opencv遍历像素的方式_opencv mat初始化_02


对于RGB图像,每个子列与通道数(3个)一致,OpenCV的顺序是BGR:


opencvsharp遍历图像像素 opencv遍历像素的方式_opencv 效率_03


通常内存足够大,可以保证一行接一行的连续存储。这样有助于加速访问每个像素。可以用isContinuous函数来判断矩阵是否是存储在连续的一块内存区域。下面是一个例子。

高效的方法 The efficient way

说到访问的访问内存的效率,C指针是第一。所有推荐下面的方法。


Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
 // 只接受uchar类型的数据 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())//若图像是连续的,整个矩阵看做一行数据,行数为1,列长度为原行数乘以原列数
    {
        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] = table[p[j]];//用lookup table赋值
        }
    }
 return I;
}


另外一种方法,可实现同样的结果。但是代码阅读性不强。


uchar* p = I.data;//如果图像导入Mat,那么I.data就是图像数据的首地址,指针指向改地址
for( unsigned int i =0; i < ncol*nrows; ++i)
    *p++ = table[*p];


迭代器(安全)方法The iterator (safe) method

为了防止访问越界或无效,采用迭代器可以安全访问像素,只需要指定begin和end。


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:
        {
            //对于三通道图像,每个像素点可以视为一个三个元素的向量,OpenCV利用Vec3b表示
            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;
}


引用返回的动态地址计算(翻译的不准确)On-the-fly address calculation with reference returning

这种方法不推荐用于遍历图像。是一种比较直观灵活的方式,可以通过设置行列坐标方式任意访问像素。


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类似,但是用来Mat_可以简化代码,一般用不到,用Mat需要用到Mat::at函数
         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;
}


核心函数 The Core Function

这是核心模块提供的用lookup table修改图像的一种方式。因为在图像处理中,经常会用给定值来修改图像的像素值。所以OpenCV提供了实现的函数。用核心模块的cv::LUT() 函数。

首先创建一个Mat类型的lookup table:


Mat lookUpTable(1, 256, CV_8U);//创建并且初始化lookup table
 uchar* p = lookUpTable.ptr();
 for( int i = 0; i < 256; ++i)
        p[i] = table[i];


然后调用函数 (I、J是输入、输出图像):


LUT(I, lookUpTable, J);


性能差异 Performance Difference

为了测试以上方法的性能差异,用了大小为 (2560 X 1600) 的图像。这里是彩色图像,为了比较结果,测试了上百次求平均值。


opencvsharp遍历图像像素 opencv遍历像素的方式_opencv 多线程_04


可以得出结论,尽可能使用OpenCV提供的函数,而不是自己编写。最快最简洁的是LUT函数,是因为OpenCV库使用了Intel TBB实现多线程。然而如果确实需要自己实现,推荐使用指针(还是离不开C)。迭代器安全但是速度慢,debug模式下的on-the-fly reference access是最慢的。在release模式下差不多,但是安全性不如迭代器。(个人看来,如果不用LUT函数,性能没有太大区别,哪个顺手用哪个,除非开销紧张)

YouTube 有官方的演示效果video posted 。