以下文本及代碼基本基於《OpenCV 2.4.13.0 documentation》的How to scan images, lookup tables and time measurement with OpenCV一節,英文好的同學可以直接看原文。
-
1. 顏色壓縮
顏色壓縮(Color Reduction)最簡單的理解就是減少表示圖像的顏色數目,我們都知道,8位位深的3通道RGB真彩圖像包括了1600多萬(16777216)的顏色數目,其實在某些應用中用不到這么多數量(例如圖像傳輸(transmission)、分割(segmentation)、壓縮(compression))的顏色。這也是一個研究的小方向,想了解更多,可以閱讀文章Adaptive Color Reduction,Color reduction and estimation of the number of dominant colors by using a self-growing and self-organized neural gas 。
在這里,我們實現一個很簡單的方法:

I_old 為輸入的像素值,I_new為輸出的像素值,divideWidth代表要減少的度,我們可以理解為當divideWidth為128的時候,對於灰度圖像就做的是一個閾值為128的二值化。
上點圖更直觀一點,左邊為灰度原始圖像,右邊為輸出圖像:
當divideWidth為128時:

當divideWidth為64時:

根據以上描述,實際上這個公式我們可以建立一個映射表來避免重復計算,對於0-255的有限的輸入值,建立輸出值的映射表:
// color space divide width
const int divideWidth = 128;
// converting table for reducing color space
uchar table[256];
// first, we should build the converting table
for (int i = 0; i < 256; i++)
{
table[i] = (uchar)(divideWidth * (i / divideWidth));
}
我們的測試程序做的就是:
1. 讀入一幅灰度圖像和一幅RGB彩色圖像;
2. 按照下文描述的四種訪問像素的方式來實現這個算法;
3. 多次分別跑算法,取平均,對四種訪問像素的方式進行對比。
-
2. 圖像數據的存儲
首先大致說明下圖像數據如何在內存中存儲。Mat是OpenCV2.x版本以上基本的圖像類型,Mat可以視為一個矩陣,矩陣的大小依賴於該Mat是什么顏色空間(Color Space),比如最基本的灰度(Gray scale)或者RGB,CMYK,YCbCr等,因為這決定了該Mat具有多少個通道,一般來講,灰度圖像只有一個通道,而RGB圖像具有三個通道。
對於灰度圖像來講,圖像數據在內存中的存儲如圖所示:

對於多通道圖像來講,有幾個通道,每一列就包含多少個子列。對於經常使用的基於RGB顏色空間,其圖像數據存儲如下:

需要注意的是通道的順序是BGR而非RGB。
一般而言,圖像數據的每一行在內存中都是連續存儲的,因為這樣對於遍歷圖像數據更高效。Mat提供了isContinuous()函數來獲取是否是連續存儲的數據。
-
3. 時間的度量
OpenCV提供了兩個簡單的函數,getTickCount()和getTickFrequency()。getTickCount返回從操作系統啟動到當前所經的計時周期數,類型為int64。getTickFrequency返回每秒的計時周期數,類型為double。因此就可以用如下的代碼計算以秒為單位的兩個操作所耗費的時間:
double dtime = (double) getTickCount(); // do something dtime = ((double)getTickCount() - dtime)/getTickFrequency();
-
4. 圖像像素的訪問方式
4.1 ptr操作和指針-高效的方式
這種方式基於.ptr和C的[]操作,這種方式也是比較推薦的遍歷圖像的方式。
/** @Method 1: the efficient method
accept grayscale image and RGB image */
int ScanImageEfficiet(Mat & image)
{
// channels of the image
int iChannels = image.channels();
// rows(height) of the image
int iRows = image.rows;
// cols(width) of the image
int iCols = image.cols * iChannels;
// check if the image data is stored continuous
if (image.isContinuous())
{
iCols *= iRows;
iRows = 1;
}
uchar* p;
for (int i = 0; i < iRows; i++)
{
// get the pointer to the ith row
p = image.ptr<uchar>(i);
// operates on each pixel
for (int j = 0; j < iCols; j++)
{
// assigns new value
p[j] = table[p[j]];
}
}
return 0;
}
這里獲取一個指向每一行的指針,然后遍歷這一行所有的數據。當圖像數據是連續存儲的時候,只需要取一次指針,然后就可以遍歷整個圖像數據。
4.2 迭代器-比較安全的方式
相較於高效的方式需要自己來計算需要遍歷的數據量,以及當圖像的行與行之間數據不連續的時候需要跳過一些間隙。迭代器(iterator)方式提供了一個更安全的訪問圖像像素的方式。你只需要做的就是聲明兩個MatIterator_變量,一個指向圖像開始,一個指向圖像結束,然后迭代。
/** @Method 2: the iterator(safe) method
accept grayscale image and RGB image */
int ScanImageIterator(Mat & image)
{
// channels of the image
int iChannels = image.channels();
switch (iChannels)
{
case 1:
{
MatIterator_<uchar> it, end;
for (it = image.begin<uchar>(), end = image.end<uchar>(); it != end; it++)
{
*it = table[*it];
}
break;
}
case 3:
{
MatIterator_<Vec3b> it, end;
for (it = image.begin<Vec3b>(), end = image.end<Vec3b>(); it != end; it++)
{
(*it)[0] = table[(*it)[0]];
(*it)[1] = table[(*it)[1]];
(*it)[2] = table[(*it)[2]];
}
break;
}
}
return 0;
}
彩色圖像的話,由於是三個通道的向量,OpenCV提供了Vec3b的數據類型來存儲。
-
4.3 動態地址計算-更適合隨機訪問的方式
這種方式不推薦用來遍歷圖像,一般用在要隨機訪問很少量的圖像數據的時候。基本用法就是指定行列號,返回該位置的像素值。不過需要你事先知道返回的數據類型是uchar還是Vec3b或者其他的。
/** @Method 3: random access method
accept grayscale image and RGB image */
int ScanImageRandomAccess(Mat & image)
{
// channels of the image
int iChannels = image.channels();
// rows(height) of the image
int iRows = image.rows;
// cols(width) of the image
int iCols = image.cols;
switch (iChannels)
{
// grayscale
case 1:
{
for (int i = 0; i < iRows; i++)
{
for (int j = 0; j < iCols; j++)
{
image.at<uchar>(i, j) = table[image.at<uchar>(i, j)];
}
}
break;
}
// RGB
case 3:
{
Mat_<Vec3b> _image = image;
for (int i = 0; i < iRows; i++)
{
for (int j = 0; j < iCols; j++)
{
_image(i, j)[0] = table[_image(i, j)[0]];
_image(i, j)[1] = table[_image(i, j)[1]];
_image(i, j)[2] = table[_image(i, j)[2]];
}
}
image = _image;
break;
}
}
return 0;
}
4.4 查找表-一顆賽艇的方式
OpenCV大概也考慮到了有很多這種需要改變單個像素值的場合(比如基於單個像素值的亮度變換,gamma矯正等),因此在core模塊提供了一個更加高效很一顆賽艇的LUT()函數來進行這種操作而且不需要遍歷整個圖像。
首先建個映射查找表:
// build a Mat type of the lookup table
Mat lookupTable(1, 256, CV_8U);
uchar* p = lookupTable.data;
for (int i = 0; i < 256; i++)
{
p[i] = table[i];
}
然后調用LUT()函數:
// call the function LUT(image, lookupTable, matout);
image是輸入圖像,matout是輸出圖像。
-
5. 不同方式的性能度量
測試環境:OpenCV版本3.1.0,Windows 7 64位系統。
測試圖像是512*512的Lena灰度圖和512*512的Lena彩色圖。分別跑100次不同的方法,然后得到的平均時間如下:
Debug版本:

Release版本:

總體時間表格如下:
|
|
灰度圖像 (Debug) |
灰度圖像 (Release) |
RGB圖像 (Debug) |
RGB圖像 (Release) |
| 高效的方式 |
1.1676 |
0.3039 |
3.5123 |
1.2646 |
| 迭代器的方式 |
151.4467 |
1.2219 |
270.9925 |
1.8997 |
| 隨機訪問的方式 |
78.6002 |
0.7484 |
328.5551 |
1.9967 |
| LUT方式 |
0.7442 |
0.1687 |
2.1805 |
0.6941 |
可以看到不管Debug版本還是Release版本,性能上都是LUT方式>高效的方式>迭代器的方式和隨機訪問的方式。Debug版本后兩種方式花費時間更大,Release版本四種方式都差別不是太大。從中也可以看出Debug版本與Release版本之間性能間的巨大差異。
參考鏈接:
