OpenCV 使用forEach進行並行畫素訪問
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 }