在面向对象编程中,遍历一个数据集合时通常使用迭代器。对于每一个集合类,标准模板类库(Standard Template Library )都有一个与之关联的迭代类。OpenCV提供的cv::Mat迭代类和C++ STL标准的迭代是兼容的。
对于cv::Mat实例的迭代对象可以由cv::MatIterator_object获得。从它的下划线可以看出它是一个模板方法。当图像迭代器在遍历图像时,返回值在编译时就应该已知。所以迭代器的声明遵循以下格式:
cv::MatIterator_<cv::Vec3b> it;
另外,你也可以使用以下方式定义:
cv::Mat_<cv::Vec3b>::iterator it;
然后使用begin和end模板方法获得起始和最终位置来遍历图像。我们改写以下上一节的colorReduce例子:
void colorReduce(cv::Mat &image, int div=64) {
// obtain iterator at initial position
cv::Mat_<cv::Vec3b>::iterator it=
image.begin<cv::Vec3b>();
// obtain end position
cv::Mat_<cv::Vec3b>::iterator itend=
image.end<cv::Vec3b>();
// loop over all pixels
for ( ; it!= itend; ++it) {
// process each pixel --------------------- (*it)[0]= (*it)[0]/div*div + div/2;
(*it)[1]= (*it)[1]/div*div + div/2;
(*it)[2]= (*it)[2]/div*div + div/2;
// end of pixel processing ---------------- }
}
在使用迭代器时,不论扫描什么样的集合时,总是遵循相同的格式。首先使用适当的类创建迭代对象。在我们的例子中是:
cv::Mat_<cv::Vec3b>::iterator (or cv::MatIterator_<cv::Vec3b>).
然后获得初始位置(在我们的例子中是图像的左上角)。使用begain方法。你也可以对迭代器使用算术方法。例如,如果你希望从图像的第二行开始,那么你可以这样初始化迭代器:
image.begin<cv::Vec3b>()+image.rows
结束位置使用end方法获得。但是如果你希望在最后一行的前一行结束,那么
image.end<cv::Vec3b>()-image.rows.
一旦你的迭代器初始化,那么就会遍历每一个元素知道达到结束位置。典型的while循环:
while (it!= itend) {
// process each pixel --------------------- …
// end of pixel processing ---------------- ++it;
}
it++可以移动到下一个元素。可以指定移动的大小,例如it+=10。
当访问元素时既可以读(element=*it;)也可以写(*it= element;)。如果想获得一个常量cv::Mat变量。可以这样声明迭代器:
cv::MatConstIterator_<cv::Vec3b> it;or cv::Mat_<cv::Vec3b>::const_iterator it;
如果图像的类型是已知的,那么begain和end方法就不用在声明类型了:
cv::Mat_<cv::Vec3b> cimage= image;
cv::Mat_<cv::Vec3b>::iterator it= cimage.begin();
cv::Mat_<cv::Vec3b>::iterator itend= cimage.end();
如果对于迭代器的工作方式还不是太了解的话,可以在网上搜索STL Iterator。
下面让我们来看一下如何更有效率的循环扫描图像
在前面的章节,我们已经提到过了不同的访问图像像素的方法。现在我们将比较一下这些不同方法的效率。因为代码的效率执行时间是我们必须要考虑的。但是代码效率最大化带来来的往往是减少了代码的可读性。所以只要适当的在核心地方增加代码的效率。
为了计算代码的执行时间,我们使用OpenCV中的cv::getTickCount()方法。这个方法给出从开机到现在的时钟周期数。因为我们想要的时间单位是毫秒,所以我们还要使用cv::getTickFrequency()方法。这个给出了每一秒的始终周期数。然后计算方法如下:
double duration;
duration = static_cast<double>(cv::getTickCount());
colorReduce(image); // the function to be tested
duration = static_cast<double>(cv::getTickCount())-duration;
duration /= cv::getTickFrequency(); // the elapsed time in ms
算出来的结果最好取多次值的平均值。
在下面的代码里,我们使用at方法来访问像素:for (int j=0; j<nl; j++) {
for (int i=0; i<nc; i++) {
// process each pixel ---------------------
image.at<cv::Vec3b>(j,i)[0]=
image.at<cv::Vec3b>(j,i)[0]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[1]=
image.at<cv::Vec3b>(j,i)[1]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[2]=
image.at<cv::Vec3b>(j,i)[2]/div*div + div/2;
// end of pixel processing ---------------- } // end of line
}
我们使用不同版本的colorReduce方法。当然不同处理器的机器执行时间不同,这里我们用的是Pentium dual core 2.2GHz。下面是我们对于一幅4288X4288像素的图像进行处理的结果:
很明显按位操作符是最快的35ms。最慢的是取余运算,用了52ms。最快比最慢的速度快乐接近50%。当需要从新将结果赋值给另外一个图像时,可以从第五行看出,计算时间增加到44ms。多余的时间是分配内存的结果。
在一个循环中,要将重复计算的东西提取到循环外一次性计算。这也是变成的一个常识,例如:
int nc= image.cols * image.channels();
…
for (int i=0; i<nc; i++) {
和
for (int i=0; i<image.cols * image.channels(); i++) {
从第六行可以看出它执行了65ms,比35ms慢了80%。
使用了迭代器的版本(第七行),执行了67ms。而迭代器的只要目的是简化图像扫描过程和出错率,因此可以不简化。
上一节中使用了at方法的运行了80ms。这个方法应该用在随机访问图像中,而绝不能用在循环中,效率太低。
往往循环次数少和多一些的循环语句比循环次数多语句少执行时间更短,尽管执行次数都是一样的。类似的,如果你有需要对像素执行N次操作。在一次循环中执行比N次循环每次执行一次效率更高。所以我们应该倾向于每次循环执行更多的操作。例如,我们前几节说到的,在遍历一个3通道的图像时,我们可以把通道数放到列数循环中去遍历:int nc= image.cols * image.channels();而不是将通道数乘以第二层循环。
然后我们就可以把colorReduce方法更改为最快的版本:
void colorReduce(cv::Mat &image, int div=64) {
int nl= image.rows; // number of lines
int nc= image.cols ; // number of columns
// is it a continous image?
if (image.isContinuous()) {
// then no padded pixels
nc= nc*nl;
nl= 1; // it is now a 1D array
}
int n= static_cast<int>(
log(static_cast<double>(div))/log(2.0));
// mask used to round the pixel value
uchar mask= 0xFF<<n; // e.g. for div=16, mask= 0xF0
// for all pixels
for (int j=0; j<nl; j++) {
// pointer to first column of line j
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// process each pixel ---------------------
*data++= *data&mask + div/2;
*data++= *data&mask + div/2;
*data++= *data&mask + div/2;
// end of pixel processing ---------------- } // end of line
}
}
这个执行时间是29ms
多线程也是增加算法效率的方法,特别是对于现在的多核处理器。OpenMP和Intel Threading Building Blocks是两个比较流行的创建和管理线程的API