OpenCV周遊圖像像素、查找表和時間效率
-
- 一、本節學習目标
- 二、測試案例
- 三、準備工作
- 四、對比三種周遊算法的性能
- 五、使用查找表的方法
- 六、性能分析
- 七、完整代碼
- 八、緻謝
一、本節學習目标
- 學會如何周遊圖像的每一個像素
- 弄懂OpenCV矩陣值是如何存儲的
- 學會評估和測量算法的性能(使用計時函數)
- 弄清楚什麼是查找表并學會使用他們
二、測試案例
讓我們考慮一個簡單的顔色量化(減少一個顔色分量如R通道的取值數)方法。通過使用unsigned char C和c++類型來存儲矩陣元素,一個像素通道可以有多達256個不同的值。對于一個三通道的圖像,這會形成太多的顔色(确切地說是1600萬)。處理如此多的色度可能會給我們的算法性能帶來嚴重的影響。然而,有時候你隻需要使用相對較少的顔色取值便能夠獲得相同的最終結果。
在這種情況下,我們通常會對顔色空間進行縮減。這意味着我們用一個新的輸入值分割顔色空間的目前值,最終得到更少的顔色取值。例如,0到9之間的每一個值都取新值0,10到19之間的每一個值都取值10,以此類推。
當你将uchar類型的(無符号char - 其值在0到255之間)值與int值相除時,結果也将是char類型的值。這些值隻能是char類型的值。是以,任何分數都會向下舍入(C/C++整除的特性,如3/2=1)。利用這一事實,uchar域上操作可以表示為:

一個簡單的顔色空間縮減算法需要周遊圖像矩陣的每個像素,在周遊的過程中應用這個公式。值得注意的是,我們做了除法和乘法運算,這些操作對于一個系統來說是很耗時的。如果可能的話,通過使用一些更高效的操作來避免它們是值得的,比如一些減法、加法,或者最好的情況是一個簡單的指派操作。此外,注意我們隻有有限數量的輸入值用于上操作。在uchar系統中,準确地說是256。
是以,對于較大的圖像,明智的做法是預先計算所有可能輸入值對應的輸出值,并在指派過程中使用查找表進行指派。查找表是簡單的數組(具有一個或多個次元),對于給定的輸入值,它儲存最終的輸出值。它的優點是我們不需要做計算,我們隻需要讀取結果。
我們的測試用例程式(以及下面的代碼示例)将執行以下操作:首先讀取指令行參數傳遞的圖像(它可能是彩色圖像也可能是灰階圖),并使用給定的指令行參數的整數值應用縮減。在OpenCV中,目前有三種主要的逐像素浏覽圖像的方法。為了讓事情變得更有趣一點,我們将使用這些方法中的每一種來處理圖像,并列印出處理所需的時間。
三、準備工作
1、建構查詢表的内容
#define divideWith 10
uchar table[256];
for (int i = 0; i < 256; ++i)
table[i] = (uchar)(divideWith * (i/divideWith));
2、計時函數統計算法性能
OpenCV提供了兩個簡單的函數來實作計時,cv::getTickCount() 和 cv::getTickFrequency()。第一個函數傳回執行該函數時系統CPU的計數(比如自啟動系統以來)。第二個函數傳回的是CPU在一秒鐘内發出多少次計數(頻率)。是以,測量兩個操作之間所花費的時間非常簡單:
double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;
3、圖像矩陣如何存儲在記憶體中?
你已經在上一節的筆記中中了解了Mat對象,矩陣的大小取決于所使用的顔色空間和圖像的尺寸(這兒隻讨論顔色系統)。顔色空間即所使用的通道數量。在灰階圖像的情況下,圖像矩陣為:
上圖中,最上一行辨別列号,最左一列辨別行号。中間的取值分别代表行列索引。
對于多通道圖像,列包含與通道數量相同的子列。例如,BGR顔色空間的圖像矩陣為:
注意通道的順序是BGR而不是RGB。通常情況下,記憶體足夠大,可以以連續的方式存儲行,是以行可能一個接一個地出現,進而建立一個長行。因為所有的東西都在一個地方,一個接着一個,這可能有助于加快掃描過程。我們可以使用 cv::Mat::isContinuous() 函數來判斷圖像矩陣的存儲是否連續。
四、對比三種周遊算法的性能
1、最有效的方式:C特點的指針操作
在性能方面,經典的C風格操作符通路是最有效的。
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
CV_Assert(I.depth() == CV_8U); // 隻接受char類型的矩陣
int channels = I.channels(); // 擷取圖像的通道數
int nRows = I.rows; // 擷取圖像的行數
// 這個列不是圖像的列數,其值與steps相等,表示圖像矩陣中一行的列數。因為圖像都是按行存儲的,
// 即一行的元素個數等于 圖像的列數 * 通道數
int nCols = I.cols * channels;
// 判斷圖像矩陣的存儲是否連續,如果連續,則可以當作一行處理,那就隻需要知道總元素個數即可
if (I.isContinuous())
{
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]];
}
}
return I;
}
這裡我們基本上隻擷取一個指向每一行開始的指針,然後周遊它直到它結束。在矩陣以連續方式存儲的特殊情況下,我們隻需要請求一次指針,并一直走到最後。我們需要處理彩色圖像:彩色圖像有三個通道,是以我們需要在每一行中周遊三倍多的元素。
還有另一種方法。Mat對象的data資料成員傳回指向第一行第一列的指針。如果該指針為空,則該對象中沒有有效的輸入。檢查這是檢查圖像加載是否成功的最簡單的方法。如果存儲是連續的,我們可以使用這個來周遊整個資料指針。在灰階圖像的情況下,如:
uchar* p = I.data;
for( unsigned int i = 0; i < ncol*nrows; ++i)
*p++ = table[*p];
你會得到相同的結果。然而,這段代碼在以後閱讀起來會困難得多。如果你在循環中做了更複雜的操作,閱讀就更困難了。而且,在實踐中,您會得到相同的性能結果(因為大多數現代編譯器可能會自動為您實作這個小的優化技巧)。
2、安全的方式:疊代器
如果采用指針的方法,則需要確定傳遞正确數量的uchar字段,并跳過行之間可能出現的間隙。疊代器方法被認為是一種更安全的方法,因為它代替使用者進行了判斷。您所需要做的就是找到圖像矩陣的開始和結束疊代器,然後遞增開始疊代器,直到結束。要擷取疊代器所指向的值,請使用*操作符(在它前面添加該值)。
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const 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]];
}
}
}
return I;
}
對于彩色圖像,每個列有三個uchar元素。這可以被認為是uchar元素組成的一個短向量,在OpenCV中它被命名為Vec3b。為了通路第n個子列,我們使用簡單的操作符[]通路。重要的是要記住,OpenCV疊代器周遊列并自動跳到下一行。是以,對于彩色圖像,如果你使用簡單的uchar疊代器,你就隻能通路藍色通道的值。
3、動态位址計算:傳回引用
最後一種方法不推薦用于周遊圖像像素。它是用來擷取或修改圖像中的随機元素的。它的基本用途是指定要通路的項的行号和列号。在前面兩種周遊方法中,你可能已經注意到:我們正在處理的圖像的類型是很重要的,這裡則不需要區分,因為您需要手動指定在自動查找中使用的類型。你可以在以下處理灰階圖像的源代碼的觀察到這一點(使用 cv::Mat::at() 函數):
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const 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;
}
}
return I;
}
該函數擷取輸入資料類型和坐标,并計算查詢項的位址,然後傳回對它的引用。在擷取值時,這可能是一個常量,而在設定值時,這可能不是常量。作為僅在debug模式中的安全步驟,将執行一個檢查,以确認您的輸入坐标是有效的,并且确實存在。如果不是這種情況,您将在标準錯誤輸出流上得到一個很好的輸出消息。與release模式下的有效方法相比,使用這個方法的唯一差別是,對于圖像的每個元素,都将獲得一個新的行指針。
如果您需要使用這種方法對一個圖像進行多次查找,那麼為每次通路輸入資料類型和at關鍵字可能會很麻煩,而且很耗時。為了解決這個問題,OpenCV使用cv::Mat_ 資料類型,它與Mat相同,隻是需要在定義時通過綁定資料矩陣來指定資料類型,但是作為回報,可以使用operator() 來快速通路元素。為了更便捷,這很容易轉換為cv::Mat資料類型。您可以在上述函數的對彩色圖像的進行中看到它的示例用法。然而要注意,同樣的操作(具有相同的運作速度)也可以用cv::Mat::at函數來完成。
五、使用查找表的方法
這是在圖像中實作查找表修改的附加方法。在圖像進行中,将所有給定的圖像值修改為其他值是很常見的。OpenCV提供了一個修改圖像值的函數,不需要寫圖像的周遊邏輯。我們使用了core子產品的 cv::LUT() 函數。首先,我們建構一個Mat類型的查找表:
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for( int i = 0; i < 256; ++i)
p[i] = table[i];
最後調用函數(I是我們的輸入圖像,J是輸出圖像):
六、性能分析
為了獲得最好的結果,請編譯程式并自己運作它。為了使不同算法的性能差異擴大,我使用了一個相當大的(3840 X 2160)圖像。這裡展示的是彩色圖像。為了獲得更精确的值,我在函數調用中調用100次取平均值。
算法 | 算法耗時(ms) |
---|---|
C指針 | 3.27054 |
MatIterator_ | 5.09794 |
Random Access | 5.4999 |
LUT | 2.68983 |
作者使用的PC環境為WIN10 64位作業系統,CPU為2塊Intel® Xeon® Silver 4114,OpenCV版本為OpenCV4.5。以上實驗資料僅供參考,每次運作可能略微不同。
七、完整代碼
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
#define DIVIDEWIDTH 10
Mat scanImageAndReduceC(Mat& src, const uchar* const table);
Mat scanImageAndReduceIterator(Mat src, const uchar* const table);
Mat scanAndReduceImageRandomAccess(Mat& src, const uchar* const table);
Mat scanAndReduceImageLUT(Mat& src, Mat& lookUpTable);
int main(int argc, char** argv)
{
cout << "----->\t\t\tStart scan and reduce image\t\t\t<-----" << endl << endl;
string fileName = "O:\\CSDN\\2.jpeg";
Mat src = imread(fileName, IMREAD_COLOR);
if (src.empty())
{
cout << "failed to load image[" << fileName << "],Please check out the path!" << endl;
system("pause");
return EXIT_FAILURE;
}
uchar table[256];
for (int i = 0; i < 256; ++i)
table[i] = (uchar)(DIVIDEWIDTH * (i / DIVIDEWIDTH));
Mat dst1, dst2, dst3, dst4;
cout << "Method No1: C point.\tDealing, please wait..." << endl;
double t1 = (double)getTickCount();
for (int i = 0; i < 100; ++i)
{
dst1= scanImageAndReduceC(src, table);
}
double t1Average = ((double)getTickCount() - t1) / getTickFrequency();
cout << "The average cost time of Method No1 is: " << t1Average << "(ms)" << endl << endl;
cout << "Method No2: C++ MatIterator_.\tDealing, please wait..." << endl;
double t2 = (double)getTickCount();
for (int i = 0; i < 100; ++i)
{
dst2 = scanImageAndReduceIterator(src, table);
}
double t2Average = ((double)getTickCount() - t2) / getTickFrequency();
cout << "The average cost time of Method No2 is: " << t2Average << "(ms)" << endl << endl;
cout << "Method No3: Random Access.\tDealing, please wait..." << endl;
double t3 = (double)getTickCount();
for (int i = 0; i < 100; ++i)
{
dst3 = scanAndReduceImageRandomAccess(src, table);
}
double t3Average = ((double)getTickCount() - t3) / getTickFrequency();
cout << "The average cost time of Method No3 is: " << t3Average << "(ms)" << endl << endl;
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for (int i = 0; i < 256; ++i)
p[i] = table[i];
cout << "Method No4: LUT.\tDealing, please wait..." << endl;
double t4 = (double)getTickCount();
for (int i = 0; i < 100; ++i)
{
dst4 = scanAndReduceImageLUT(src, lookUpTable);
}
double t4Average = ((double)getTickCount() - t4) / getTickFrequency();
cout << "The average cost time of Method No4 is: " << t4Average << "(ms)" << endl << endl;
namedWindow("Original", WINDOW_NORMAL);
resizeWindow("Original", src.cols / 5, src.rows / 5);
imshow("Original", src);
namedWindow("C point", WINDOW_NORMAL);
resizeWindow("C point", src.cols / 5, src.rows / 5);
imshow("C point", dst1);
namedWindow("MatIterator_", WINDOW_NORMAL);
resizeWindow("MatIterator_", src.cols / 5, src.rows / 5);
imshow("MatIterator_", dst2);
namedWindow("Random access", WINDOW_NORMAL);
resizeWindow("Random access", src.cols / 5, src.rows / 5);
imshow("Random access", dst3);
namedWindow("LUT", WINDOW_NORMAL);
resizeWindow("LUT", src.cols / 5, src.rows / 5);
imshow("LUT", dst4);
waitKey(0);
destroyAllWindows();
system("pause");
return EXIT_SUCCESS;
}
Mat scanImageAndReduceC(Mat& src, const uchar* const table)
{
Mat I = src.clone();
CV_Assert(I.depth() == CV_8U);
int channels = I.channels();
int rows = I.rows;
int cols = I.cols * channels;
if (I.isContinuous())
{
cols *= rows;
rows = 1;
}
uchar* p;
for (int row = 0; row < rows; ++row)
{
p = I.ptr<uchar>(row);
for (int col = 0; col < cols; ++col)
{
p[col] = table[p[col]];
}
}
return I;
}
Mat scanImageAndReduceIterator(Mat src, const uchar* const table)
{
Mat I = src.clone();
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]];
}
break;
}
}
return I;
}
Mat scanAndReduceImageRandomAccess(Mat& src, const uchar* const table)
{
Mat I = src.clone();
CV_Assert(I.depth() == CV_8U);
const int channels = I.channels();
switch (channels)
{
case 1:
{
for (int row = 0; row < I.rows; ++row)
for (int col = 0; col < I.cols; ++col)
I.at<uchar>(row, col) = table[I.at<uchar>(row, col)];
break;
}
case 3:
{
Mat_<Vec3b> _I = I;
for(int row=0;row<I.rows;++row)
for (int col = 0; col < I.cols; ++col)
{
_I(row, col)[0] = table[_I(row, col)[0]];
_I(row, col)[1] = table[_I(row, col)[1]];
_I(row, col)[2] = table[_I(row, col)[2]];
}
break;
}
}
return I;
}
Mat scanAndReduceImageLUT(Mat& src,Mat &lookUpTable)
{
Mat I = src.clone();
CV_Assert(I.depth() == CV_8U);
LUT(I, lookUpTable, I);
return I;
}
八、緻謝
1、感謝自己堅持不懈地做好每件事
2、感謝OpenCV官方團隊作出地貢獻
3、感興趣的小夥伴歡迎入群讨論學習。入群飛機票