OpenCV中使用forEach进行并行像素访问
在本教程中,我们将比较Mat类的forEach方法的性能和访问和转换OpenCV中像素值的其他方式。 我们将展示如何使用at方法甚至高效地使用指针算法,forEach比使用at方法快得多。
OpenCV中有隐藏的宝石,有时不是很知名。 其中一个隐藏的宝石是Mat类的forEach方法,它利用机器上的所有内核在每个像素上应用任何函数。
让我们先定义一个函数complexThreshold。 它采用一个RGB像素值并对其应用一个复杂的阈值。
1 // Define a pixel
2 typedef Point3_<uint8_t> Pixel; 3
4 // A complicated threshold is defined so 5 // a non-trivial amount of computation 6 // is done at each pixel.
7 void complicatedThreshold(Pixel &pixel) 8 { 9 if (pow(double(pixel.x)/10,2.5) > 100) 10 { 11 pixel.x = 255; 12 pixel.y = 255; 13 pixel.z = 255; 14 } 15 else
16 { 17 pixel.x = 0; 18 pixel.y = 0; 19 pixel.z = 0; 20 } 21 }
与简单的阈值相比,这个函数在计算上要重得多。 这样,我们不仅仅是测试像素访问时间,而且每个像素操作的计算量都很大时,forEach如何使用所有内核。
接下来,我们将通过四种不同的方式将这个函数应用到图像中的每个像素,并检查相关的性能。
方法1:使用at方法的朴素像素访问
Mat类有一个方便的方法来访问图像中位置(行,列)的像素。 以下代码使用at方法来访问每个像素并将复杂的阈值应用于它。
1 // Naive pixel access 2 // Loop over all rows
3 for (int r = 0; r < image.rows; r++) 4 { 5 // Loop over all columns
6 for ( int c = 0; c < image.cols; c++) 7 { 8 // Obtain pixel at (r, c)
9 Pixel pixel = image.at<Pixel>(r, c); 10 // Apply complicatedTreshold
11 complicatedThreshold(pixel); 12 // Put result back
13 image.at<Pixel>(r, c) = pixel; 14 } 15 }
上面的方法被认为是低效的,因为每次我们调用at方法时,内存中像素的位置正在被计算。 这涉及乘法操作。 不使用像素位于连续的存储器块中的事实。
方法2:使用指针算法进行像素访问
在OpenCV中,一行中的所有像素都存储在一个连续的内存块中。 如果使用create创建了Mat对象,则所有像素都存储在一个连续的内存块中。 由于我们正在从磁盘读取图像,imread使用create方法,因此我们可以简单地使用不需要乘法的指针运算来遍历所有像素。
代码如下所示。
1 // Using pointer arithmetic 2
3 // Get pointer to first pixel
4 Pixel* pixel = image1.ptr<Pixel>(0,0); 5
6 // Mat objects created using the create method are stored 7 // in one continous memory block.
8 const Pixel* endPixel = pixel + image1.cols * image1.rows; 9 // Loop over all pixels
10 for (; pixel != endPixel; pixel++) 11 { 12 complicatedThreshold(*pixel); 13 }
方法3:使用forEach
Mat类的forEach方法接受一个函数操作符。 用法是
void cv::Mat::forEach (const Functor &operation)
了解上述用法的最简单的方法是通过下面的示例。 我们定义了一个用于forEach的函数对象(Operator)。
1 // Parallel execution with function object.
2 struct Operator 3 { 4 void operator ()(Pixel &pixel, const int * position) const
5 { 6 // Perform a simple threshold operation
7 complicatedThreshold(pixel); 8 } 9 };
调用forEach很简单,只需要一行代码即可完成
1 // Call forEach
2 image2.forEach<Pixel>(Operator());
方法4:在C ++ 11 Lambda中使用forEach
1 image3.forEach<Pixel>
2 ( 3 [](Pixel &pixel, const int * position) -> void
4 { 5 complicatedThreshold(pixel); 6 } 7 );
比较forEach的性能
复杂阈值函数连续五次应用于大小为9000 x 6750的大图像的所有像素。 实验中使用的2.5 GHz Intel Core i7处理器有四个内核。 以下时间已经获得。 请注意,使用forEach比使用Naive Pixel Access或Pointer Arithmetic方法快五倍。
Method Type | Time ( milliseconds ) |
---|---|
Naive Pixel Access | 6656 |
Pointer Arithmetic | 6575 |
forEach | 1221 |
forEach (C++11 Lambda) | 1272 |
我已经在OpenCV中编写了十多年的代码,每当我必须编写访问像素的优化代码时,我都会使用指针算法而不是naive 的方法。 不过,在写这篇博文的时候,我惊讶地发现,即使是大图片,这两种方法之间似乎也没有什么区别。
完整代码:
1 // Include OpenCV header
2 #include <opencv2/opencv.hpp>
3
4 // Use cv and std namespaces
5 using namespace cv; 6 using namespace std; 7
8 // Define a pixel
9 typedef Point3_<uint8_t> Pixel; 10
11 // tic is called to start timer
12 void tic(double &t) 13 { 14 t = (double)getTickCount(); 15 } 16
17 // toc is called to end timer
18 double toc(double &t) 19 { 20 return ((double)getTickCount() - t) / getTickFrequency(); 21 } 22
23 void complicatedThreshold(Pixel &pixel) 24 { 25 if (pow(double(pixel.x) / 10, 2.5) > 100) 26 { 27 pixel.x = 255; 28 pixel.y = 255; 29 pixel.z = 255; 30 } 31 else
32 { 33 pixel.x = 0; 34 pixel.y = 0; 35 pixel.z = 0; 36 } 37 } 38
39
40
41 // Parallel execution with function object.
42 struct Operator 43 { 44 void operator ()(Pixel &pixel, const int * position) const
45 { 46 // Perform a simple threshold operation
47 complicatedThreshold(pixel); 48 } 49 }; 50
51
52 int main(int argc, char** argv) 53 { 54 // Read image
55 Mat image = imread("butterfly.jpg"); 56
57 // Scale image 30x
58 resize(image, image, Size(), 30, 30); 59
60 // Print image size
61 cout << "Image size " << image.size() << endl; 62
63 // Number of trials
64 int numTrials = 5; 65
66 // Print number of trials
67 cout << "Number of trials : " << numTrials << endl; 68
69 // Make two copies
70 Mat image1 = image.clone(); 71 Mat image2 = image.clone(); 72 Mat image3 = image.clone(); 73
74 // Start timer
75 double t; 76 tic(t); 77
78 for (int n = 0; n < numTrials; n++) 79 { 80 // Naive pixel access 81 // Loop over all rows
82 for (int r = 0; r < image.rows; r++) 83 { 84 // Loop over all columns
85 for (int c = 0; c < image.cols; c++) 86 { 87 // Obtain pixel at (r, c)
88 Pixel pixel = image.at<Pixel>(r, c); 89 // Apply complicatedTreshold
90 complicatedThreshold(pixel); 91 // Put result back
92 image.at<Pixel>(r, c) = pixel; 93 } 94
95 } 96 } 97
98 cout << "Naive way: " << toc(t) << endl; 99
100
101 // Start timer
102 tic(t); 103
104 // image1 is guaranteed to be continous, but 105 // if you are curious uncomment the line below 106 // cout << "Image 1 is continous : " << image1.isContinuous() << endl;
107
108 for (int n = 0; n < numTrials; n++) 109 { 110 // Get pointer to first pixel
111 Pixel* pixel = image1.ptr<Pixel>(0, 0); 112
113 // Mat objects created using the create method are stored 114 // in one continous memory block.
115 const Pixel* endPixel = pixel + image1.cols * image1.rows; 116
117 // Loop over all pixels
118 for (; pixel != endPixel; pixel++) 119 { 120 complicatedThreshold(*pixel); 121 } 122
123
124 } 125 cout << "Pointer Arithmetic " << toc(t) << endl; 126 tic(t); 127
128 for (int n = 0; n < numTrials; n++) 129 { 130 image2.forEach<Pixel>(Operator()); 131 } 132 cout << "forEach : " << toc(t) << endl; 133
134 #if __cplusplus >= 201103L || (__cplusplus < 200000 && __cplusplus > 199711L)
135 tic(t); 136
137 for (int n = 0; n < numTrials; n++) 138 { 139 // Parallel execution using C++11 lambda.
140 image3.forEach<Pixel>
141 ( 142 [](Pixel &pixel, const int * position) -> void
143 { 144 complicatedThreshold(pixel); 145 } 146 ); 147 } 148 cout << "forEach C++11 : " << toc(t) << endl; 149
150 #endif
151
152 return EXIT_SUCCESS; 153 }