1 圖像的表示
在正式介紹之前,先簡單介紹一下數(shù)字圖像的基本概念。如圖 1-1 中所示 的圖像,我們看到的是 Lena 的頭像,但是計算機看來,這副圖像只是一堆亮度各異的點。一副尺寸為 M × N 的圖像可以用一個 M × N 的矩陣來表示,矩陣元素的值表示這個位置上的像素的亮度,一般來說像素值越大表示該點越 亮。如圖 3.1 中白色圓圈內(nèi)的區(qū)域,進行放大并仔細(xì)查看,將會如圖 1-2 所


一般來說,灰度圖用2維矩陣表示,彩色(多通道)圖像用3 維矩陣(M × N × 3)表示。對于圖像顯示來說,目前大部分設(shè)備都是用無符號 8 位整 數(shù)(類型為 CV_8U)表示像素亮度。
圖像數(shù)據(jù)在計算機內(nèi)存中的存儲順序為以圖像最左上點(也可能是最左下 點)開始,存儲如表1-1 所示。

Iij 表示第 i 行 j 列的像素值。如果是多通道圖像,比如 RGB 圖像,則每個 像素用三個字節(jié)表示。在 OpenCV 中,RGB 圖像的通道順序為 BGR ,存儲如 表 1-2 所示。

2 Mat 類
早期的 OpenCV 中,使用 IplImage 和 CvMat 數(shù)據(jù)結(jié)構(gòu)來表示圖像。IplImage 和 CvMat 都是 C 語言的結(jié)構(gòu)。使用這兩個結(jié)構(gòu)的問題是內(nèi)存需要手動管理,開 發(fā)者必須清楚的知道何時需要申請內(nèi)存,何時需要釋放內(nèi)存。這個開發(fā)者帶來了 一定的負(fù)擔(dān),開發(fā)者應(yīng)該將更多精力用于算法設(shè)計,因此在新版本的 OpenCV 中 引入了 Mat 類。
新加入的 Mat 類能夠自動管理內(nèi)存。使用 Mat 類,你不再需要花費大量精 力在內(nèi)存管理上。而且你的代碼會變得很簡潔,代碼行數(shù)會變少。但 C++接口唯 一的不足是當(dāng)前一些嵌入式開發(fā)系統(tǒng)可能只支持 C 語言,如果你的開發(fā)平臺支持 C++,完全沒有必要再用 IplImage 和 CvMat。在新版本的 OpenCV 中,開發(fā)者依 然可以使用 IplImage 和 CvMat,但是一些新增加的函數(shù)只提供了 Mat 接口。以后的博客中的例程也都將采用新的 Mat 類,不再介紹 IplImage 和 CvMat。
Mat 類的定義如下所示,關(guān)鍵的屬性如下方代碼所示:
class CV_EXPORTS Mat
{
public:
//一系列函數(shù)
...
/* flag參數(shù)中包含許多關(guān)于矩陣的信息,如:
-Mat 的標(biāo)識
-數(shù)據(jù)是否連續(xù)
-深度
-通道數(shù)目
*/
int flags;
//矩陣的維數(shù),取值應(yīng)該大于或等于 2
int dims;
//矩陣的行數(shù)和列數(shù),如果矩陣超過 2 維,這兩個變量的值都為-1
int rows, cols;
//指向數(shù)據(jù)的指針
uchar* data;
//指向引用計數(shù)的指針
//如果數(shù)據(jù)是由用戶分配的,則為 NULL
int* refcount;
//其他成員變量和成員函數(shù)
... };
3 UIImage 與mat之間的轉(zhuǎn)換
由于ios平臺不能使用imread() 進行讀取圖片,因此需要將進行單獨轉(zhuǎn)換
3.1 UIImage轉(zhuǎn)換成Mat
- (cv::Mat)cvMatFromUIImage:(UIImage *)image
{
CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
CGFloat cols = image.size.width;
CGFloat rows = image.size.height;
cv::Mat cvMat(rows, cols, CV_8UC4); // 8 bits per component, 4 channels (color channels + alpha)
CGContextRef contextRef = CGBitmapContextCreate(cvMat.data, // Pointer to data
cols, // Width of bitmap
rows, // Height of bitmap
8, // Bits per component
cvMat.step[0], // Bytes per row
colorSpace, // Colorspace
kCGImageAlphaNoneSkipLast |
kCGBitmapByteOrderDefault); // Bitmap info flags
CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
CGContextRelease(contextRef);
Mat src;
cvtColor(cvMat, src, COLOR_BGR2RGB);
return src;
}
上面只是簡單的轉(zhuǎn)換其實并不完全正確,沒檢查UIIimage的具體排列樣式
3.2 Mat轉(zhuǎn)換成UIImage
-(UIImage *)UIImageFromCVMat:(cv::Mat)cvMat
{
Mat src;
cvtColor(cvMat, src, COLOR_BGR2RGB);
NSData *data = [NSData dataWithBytes:src.data length:src.elemSize()*src.total()];
CGColorSpaceRef colorSpace;
if (cvMat.elemSize() == 1) {
colorSpace = CGColorSpaceCreateDeviceGray();
} else {
colorSpace = CGColorSpaceCreateDeviceRGB();
}
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
// Creating CGImage from cv::Mat
CGImageRef imageRef = CGImageCreate(cvMat.cols, //width
cvMat.rows, //height
8, //bits per component
8 * cvMat.elemSize(), //bits per pixel
cvMat.step[0], //bytesPerRow
colorSpace, //colorspace
kCGImageAlphaNone|kCGBitmapByteOrderDefault,// bitmap info
provider, //CGDataProviderRef
NULL, //decode
false, //should interpolate
kCGRenderingIntentDefault //intent
);
// Getting UIImage from CGImage
UIImage *finalImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CGDataProviderRelease(provider);
CGColorSpaceRelease(colorSpace);
return finalImage;
}
Mat 默認(rèn)顏色空間是BGR ,而UIImage 默認(rèn)顏色空間是RGB .因此 我們需要將Mat的BGR轉(zhuǎn)換成RGB 再轉(zhuǎn)換成UIImage ,同理,UIImage 也需要將UIImage 轉(zhuǎn)換成Mat 的RGB再轉(zhuǎn)換成BGR 再使用
4創(chuàng)建 Mat 對象
Mat 是一個非常優(yōu)秀的圖像類,它同時也是一個通用的矩陣類,可以用來創(chuàng)
建和操作多維矩陣。有多種方法創(chuàng)建一個 Mat 對象。
4.1 構(gòu)造函數(shù)方法
Mat 類提供了一系列構(gòu)造函數(shù),可以方便的根據(jù)需要創(chuàng)建 Mat 對象。下面是 一個使用構(gòu)造函數(shù)創(chuàng)建對象的例子。
Mat M(3,2, CV_8UC3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl;
使用cout 輸出到控制臺需要使用命令空間
using namespace std;
第一行代碼創(chuàng)建一個行數(shù)(高度)為 3,列數(shù)(寬度)為 2 的圖像,圖像元 素是 8 位無符號整數(shù)類型,且有三個通道。圖像的所有像素值被初始化為(0, 0, 255)。由于 OpenCV 中默認(rèn)的顏色順序為 BGR,因此這是一個全紅色的圖像。

第二行代碼是輸出 Mat 類的實例 M 的所有像素值。Mat 重定義了<<操作符, 使用這個操作符,可以方便地輸出所有像素值,而不需要使用 for 循環(huán)逐個像素 輸出。
該段代碼的輸出結(jié)果如下
M =
[ 0, 0, 255, 0, 0, 255;
0, 0, 255, 0, 0, 255;
0, 0, 255, 0, 0, 255]
常用的構(gòu)造函數(shù)有:
- Mat::Mat()
無參數(shù)構(gòu)造方法; - Mat::Mat(int rows, int cols, int type)
創(chuàng)建行數(shù)為 rows,列數(shù)為 col,類型為 type 的圖像; - Mat::Mat(Size size, int type)
創(chuàng)建大小為 size,類型為 type 的圖像; - Mat::Mat(int rows, int cols, int type, const Scalar& s)
創(chuàng)建行數(shù)為 rows,列數(shù)為 col,類型為 type 的圖像,并將所有元素初始 化為值 s; - Mat::Mat(Size size, int type, const Scalar& s)
創(chuàng)建大小為 size,類型為 type 的圖像,并將所有元素初始化為值 s; - Mat::Mat(const Mat& m)
將 m 賦值給新創(chuàng)建的對象,此處不會對圖像數(shù)據(jù)進行復(fù)制,m 和新對象 共用圖像數(shù)據(jù); - Mat::Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP)
創(chuàng)建行數(shù)為 rows,列數(shù)為 col,類型為 type 的圖像,此構(gòu)造函數(shù)不創(chuàng)建 圖像數(shù)據(jù)所需內(nèi)存,而是直接使用 data 所指內(nèi)存,圖像的行步長由 step 指定。 - Mat::Mat(Size size, int type, void* data, size_t step=AUTO_STEP)
創(chuàng)建大小為 size,類型為 type 的圖像,此構(gòu)造函數(shù)不創(chuàng)建圖像數(shù)據(jù)所需 內(nèi)存,而是直接使用 data 所指內(nèi)存,圖像的行步長由 step 指定。 - Mat::Mat(const Mat& m, const Range& rowRange, const Range& colRange)
創(chuàng)建的新圖像為 m 的一部分,具體的范圍由 rowRange 和 colRange 指 定,此構(gòu)造函數(shù)也不進行圖像數(shù)據(jù)的復(fù)制操作,新圖像與 m 共用圖像數(shù) 據(jù); - Mat::Mat(const Mat& m, const Rect& roi)
創(chuàng)建的新圖像為 m 的一部分,具體的范圍 roi 指定,此構(gòu)造函數(shù)也不進 行圖像數(shù)據(jù)的復(fù)制操作,新圖像與 m 共用圖像數(shù)據(jù)。
這些構(gòu)造函數(shù)中,很多都涉及到類型 type。
type 可以是 CV_8UC1,CV_16SC1,..., CV_64FC4等。里面的 8U 表示 8 位無符號整數(shù),16S 表示 16 位有符號整數(shù),64F 表示 64 位浮點數(shù)(即 double 類型);C 后面的數(shù)表示通道數(shù),例如 C1 表示一個 通道的圖像,C4 表示 4 個通道的圖像,以此類推。
如果你需要更多的通道數(shù),需要用宏 CV_8UC(n),例如:
Mat M(3,2, CV_8UC(5));//創(chuàng)建行數(shù)為3,列數(shù)為2,通道數(shù)為5的圖像
4.2create()函數(shù)創(chuàng)建對象
除了在構(gòu)造函數(shù)中可以創(chuàng)建圖像,也可以使用 Mat 類的 create()函數(shù)創(chuàng)建圖 像。如果 create()函數(shù)指定的參數(shù)與圖像之前的參數(shù)相同,則不進行實質(zhì)的內(nèi)存 申請操作;如果參數(shù)不同,則減少原始數(shù)據(jù)內(nèi)存的索引,并重新申請內(nèi)存。使用 方法如下面例程所示:
Mat M(2,2, CV_8UC3);//構(gòu)造函數(shù)創(chuàng)建圖像
M.create(3,2, CV_8UC2);//釋放內(nèi)存重新創(chuàng)建圖像
需要注意的時,使用 create()函數(shù)無法設(shè)置圖像像素的初始值。
測試代碼
Mat M(2,2, CV_8UC3,Scalar(0,0,0));//創(chuàng)建紅色
cout << "M = " << endl << " " << M << endl;
M.create(3,2, CV_8UC4);//create 方式創(chuàng)建
cout << "M = " << endl << " " << M << endl;
結(jié)果
M =
[ 0, 0, 255, 0, 0, 255;
0, 0, 255, 0, 0, 255]
M =
[ 0, 0, 0, 0, 91, 0, 0, 0;
160, 197, 31, 0, 0, 96, 0, 0;
0, 0, 0, 0, 0, 0, 0, 112]
4.3 Matlab風(fēng)格的創(chuàng)建對象方法
OpenCV 2 中提供了 Matlab 風(fēng)格的函數(shù),如 zeros(),ones()和 eyes()。這種方 法使得代碼非常簡潔,使用起來也非常方便。使用這些函數(shù)需要指定圖像的大小 和類型,使用方法如下:
Mat Z = Mat::zeros(2,3, CV_8UC1);
cout << "Z = " << endl << " " << Z << endl;
Mat O = Mat::ones(2, 3, CV_32F);
cout << "O = " << endl << " " << O << endl;
Mat E = Mat::eye(2, 3, CV_64F);
cout << "E = " << endl << " " << E << endl;
該代碼中,有些 type 參數(shù)如 CV_32F 未注明通道數(shù)目,這種情況下它表示單 通道。上面代碼的輸出結(jié)果如下所示。
Z =
[ 0, 0, 0;
0, 0, 0]
O =
[1, 1, 1;
1, 1, 1]
E =
[1, 0, 0;
0, 1, 0]
5 矩陣的基本元素表達
對于單通道圖像,其元素類型一般為 8U(即 8 位無符號整數(shù)),當(dāng)然也可以 是 16S、32F 等;這些類型可以直接用 uchar、short、float 等 C/C++語言中的基本 數(shù)據(jù)類型表達。
如果多通道圖像,如 RGB 彩色圖像,需要用三個通道來表示。在這種情況 下,如果依然將圖像視作一個二維矩陣,那么矩陣的元素不再是基本的數(shù)據(jù)類型。
OpenCV 中有模板類 Vec,可以表示一個向量。OpenCV 中使用 Vec 類預(yù)定義了一 些小向量,可以將之用于矩陣元素的表達。
typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;
typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;
typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;
typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;
例如 8U 類型的 RGB 彩色圖像可以使用 Vec3b,3 通道 float 類型的矩陣可以 使用 Vec3f。
對于 Vec 對象,可以使用[]符號如操作數(shù)組般讀寫其元素,如:
Vec3b color; //用color變量描述一種RGB顏色
color[0]=255; //B分量
color[1]=0; //G分量
color[2]=0; //R分量
6 像素值的讀寫
很多時候,我們需要讀取某個像素值,或者設(shè)置某個像素值;在更多的時候, 我們需要對整個圖像里的所有像素進行遍歷。OpenCV 提供了多種方法來實現(xiàn)圖 像的遍歷。
6.1 at()函數(shù)
函數(shù) at()來實現(xiàn)讀去矩陣中的某個像素,或者對某個像素進行賦值操作。下
面兩行代碼演示了 at()函數(shù)的使用方法。
uchar value = grayim.at<uchar>(i,j);//讀出第i行第j列像素值
grayim.at<uchar>(i,j)=128; //將第i行第j列像素值設(shè)置為128
如果要對圖像進行遍歷,可以參考下面的例程。這個例程創(chuàng)建了兩個圖像, 分別是單通道的 grayim 以及 3 個通道的 colorim,然后對兩個圖像的所有像素值 進行賦值,最后現(xiàn)實結(jié)果。
-(void)atFunction{
Mat grayim(600, 800, CV_8UC1);
Mat colorim(600, 800, CV_8UC3);
for( int i = 0; i < grayim.rows; ++i){
for( int j = 0; j < grayim.cols; ++j ){
grayim.at<uchar>(i,j) = (i+j)%255;
}
}
//遍歷所有像素,并設(shè)置像素值
for( int i = 0; i < colorim.rows; ++i){
for( int j = 0; j < colorim.cols; ++j ) {
Vec3b pixel;
pixel[0] = i%255; //Blue
pixel[1] = j%255; //Green
pixel[2] = 0; //Red
colorim.at<Vec3b>(i,j) = pixel;
}
}
UIImageView * imageView;
imageView = [self createImageViewInRect:CGRectMake(0, 300, 100, 100)];
[self.view addSubview:imageView];
imageView.image = [self UIImageFromCVMat:grayim];
imageView = [self createImageViewInRect:CGRectMake(0, 400, 100, 100)];
[self.view addSubview:imageView];
imageView.image = [self UIImageFromCVMat:colorim];
}
-(UIImage *)UIImageFromCVMat:(cv::Mat)cvMat
{
// mat 是brg 而 rgb
Mat src;
NSData *data=nil;
CGColorSpaceRef colorSpace;
if (cvMat.elemSize() == 1) {
colorSpace = CGColorSpaceCreateDeviceGray();
data= [NSData dataWithBytes:cvMat.data length:cvMat.elemSize()*cvMat.total()];
} else {
cvtColor(cvMat, src, COLOR_BGR2RGB);
data= [NSData dataWithBytes:src.data length:src.elemSize()*src.total()];
colorSpace = CGColorSpaceCreateDeviceRGB();
}
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
// Creating CGImage from cv::Mat
CGImageRef imageRef = CGImageCreate(cvMat.cols, //width
cvMat.rows, //height
8, //bits per component
8 * cvMat.elemSize(), //bits per pixel
cvMat.step[0], //bytesPerRow
colorSpace, //colorspace
kCGImageAlphaNone|kCGBitmapByteOrderDefault,// bitmap info
provider, //CGDataProviderRef
NULL, //decode
false, //should interpolate
kCGRenderingIntentAbsoluteColorimetric //intent
);
// Getting UIImage from CGImage
UIImage *finalImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CGDataProviderRelease(provider);
CGColorSpaceRelease(colorSpace);
return finalImage;
}
結(jié)果如下

需要注意的是,如果要遍歷圖像,并不推薦使用 at()函數(shù)。使用這個函數(shù)的 優(yōu)點是代碼的可讀性高,但是效率并不是很高。
6.2 使用迭代器
如果你熟悉 C++的 STL 庫,那一定了解迭代器(iterator)的使用。迭代器可 以方便地遍歷所有元素。Mat 也增加了迭代器的支持,以便于矩陣元素的遍歷。 下面的例程功能跟上一節(jié)的例程類似,但是由于使用了迭代器,而不是使用行數(shù) 和列數(shù)來遍歷,所以這兒沒有了 i 和 j 變量,圖像的像素值設(shè)置為一個隨機數(shù)。
-(void)IteratorFunction{
Mat grayim(600, 800, CV_8UC1);
Mat colorim(600, 800, CV_8UC3);
MatIterator_<uchar> grayit, grayend;
for( grayit = grayim.begin<uchar>(),grayend=grayim.end<uchar>(); grayit != grayend; ++grayit){
*grayit = rand()%255;
}
//遍歷所有像素,并設(shè)置像素值
MatIterator_<Vec3b> colorit, colorend;
for( colorit = colorim.begin<Vec3b>(),colorend=colorim.end<Vec3b>(); colorit != colorend; ++colorit) {
(*colorit)[0] = rand()%255; //Blue
(*colorit)[1] = rand()%255; //Green
(*colorit)[2] = rand()%255; //Red
}
UIImageView * imageView;
imageView = [self createImageViewInRect:CGRectMake(100, 300, 100, 100)];
[self.view addSubview:imageView];
imageView.image = [self UIImageFromCVMat:grayim];
imageView = [self createImageViewInRect:CGRectMake(100, 400, 100, 100)];
[self.view addSubview:imageView];
imageView.image = [self UIImageFromCVMat:colorim];
}
-(UIImage *)UIImageFromCVMat:(cv::Mat)cvMat
{
// mat 是brg 而 rgb
Mat src;
NSData *data=nil;
CGColorSpaceRef colorSpace;
if (cvMat.elemSize() == 1) {
colorSpace = CGColorSpaceCreateDeviceGray();
data= [NSData dataWithBytes:cvMat.data length:cvMat.elemSize()*cvMat.total()];
} else {
cvtColor(cvMat, src, COLOR_BGR2RGB);
data= [NSData dataWithBytes:src.data length:src.elemSize()*src.total()];
colorSpace = CGColorSpaceCreateDeviceRGB();
}
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
// Creating CGImage from cv::Mat
CGImageRef imageRef = CGImageCreate(cvMat.cols, //width
cvMat.rows, //height
8, //bits per component
8 * cvMat.elemSize(), //bits per pixel
cvMat.step[0], //bytesPerRow
colorSpace, //colorspace
kCGImageAlphaNone|kCGBitmapByteOrderDefault,// bitmap info
provider, //CGDataProviderRef
NULL, //decode
false, //should interpolate
kCGRenderingIntentAbsoluteColorimetric //intent
);
// Getting UIImage from CGImage
UIImage *finalImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CGDataProviderRelease(provider);
CGColorSpaceRelease(colorSpace);
return finalImage;
}
結(jié)果如下

6.3 通過數(shù)據(jù)指針
使用 IplImage 結(jié)構(gòu)的時候,我們會經(jīng)常使用數(shù)據(jù)指針來直接操作像素。通過 指針操作來訪問像素是非常高效的,但是你務(wù)必十分地小心。C/C++中的指針操 作是不進行類型以及越界檢查的,如果指針訪問出錯,程序運行時有時候可能看上去一切正常,有時候卻突然彈出“段錯誤”(segment fault)。
當(dāng)程序規(guī)模較大,且邏輯復(fù)雜時,查找指針錯誤十分困難。對于不熟悉指針 的編程者來說,指針就如同噩夢。如果你對指針使用沒有自信,則不建議直接通 過指針操作來訪問像素。雖然 at()函數(shù)和迭代器也不能保證對像素訪問進行充分 的檢查,但是總是比指針操作要可靠一些。
如果你非常注重程序的運行速度,那么遍歷像素時,建議使用指針。下面的 例程演示如何使用指針來遍歷圖像中的所有像素。此例程實現(xiàn)的操作跟第 6.1 節(jié)中的例程完全相同。例程代碼如下:
-(void)pointFunction{
Mat grayim(600, 800, CV_8UC1);
Mat colorim(600, 800, CV_8UC3);
for( int i = 0; i < grayim.rows; ++i) {
//獲取第 i 行首像素指針
uchar * p = grayim.ptr<uchar>(i);
//對第 i 行的每個像素(byte)操作
for( int j = 0; j < grayim.cols; ++j )
p[j] = (i+j)%255;
}
//遍歷所有像素,并設(shè)置像素值
for( int i = 0; i < colorim.rows; ++i) {
//獲取第 i 行首像素指針
Vec3b * p = colorim.ptr<Vec3b>(i);
for( int j = 0; j < colorim.cols; ++j ) {
p[j][0] = i%255; //Blue
p[j][1] = j%255; //Green
p[j][2] = 0; //Red
}
}
UIImageView * imageView;
imageView = [self createImageViewInRect:CGRectMake(100, 100, 100, 100)];
[self.view addSubview:imageView];
imageView.image = [self UIImageFromCVMat:grayim];
imageView = [self createImageViewInRect:CGRectMake(200, 100, 100, 100)];
[self.view addSubview:imageView];
imageView.image = [self UIImageFromCVMat:colorim];
}
結(jié)果如圖

7 選取圖像局部區(qū)域
Mat 類提供了多種方便的方法來選擇圖像的局部區(qū)域。使用這些方法時需要 注意,這些方法并不進行內(nèi)存的復(fù)制操作。如果將局部區(qū)域賦值給新的 Mat 對 象,新對象與原始對象共用相同的數(shù)據(jù)區(qū)域,不新申請內(nèi)存,因此這些方法的執(zhí) 行速度都比較快。
7.1 單行或單列選擇
提取矩陣的一行或者一列可以使用函數(shù) row()或 col()。函數(shù)的聲明如下:
Mat Mat::row(int i) const
Mat Mat::col(int j) const
參數(shù) i 和 j 分別是行標(biāo)和列標(biāo)。例如取出 A 矩陣的第 i 行可以使用如下代碼:
Mat line = A.row(i);
例如取出 A 矩陣的第 i 行,將這一行的所有元素都乘以 2,然后賦值給第 j行,可以這樣寫:
A.row(j) = A.row(i)*2;
測試代碼
-(void)rowOpateration{
Mat grayim(2, 2, CV_8UC1);
for( int i = 0; i < grayim.rows; ++i) {
//獲取第 i 行首像素指針
uchar * p = grayim.ptr<uchar>(i);
//對第 i 行的每個像素(byte)操作
for( int j = 0; j < grayim.cols; ++j )
p[j] = 100;
}
UIImageView * imageView;
imageView = [self createImageViewInRect:CGRectMake(100, 100, 100, 100)];
[self.view addSubview:imageView];
imageView.image = [self UIImageFromCVMat:grayim];
grayim.row(1) = grayim.row(0)*2;
imageView = [self createImageViewInRect:CGRectMake(200, 100, 100, 100)];
[self.view addSubview:imageView];
imageView.image = [self UIImageFromCVMat:grayim];
}
測試結(jié)果

7.2 用Range選擇多行或多列
Range 是 OpenCV 中新增的類,該類有兩個關(guān)鍵變量 star 和 end。Range 對 象可以用來表示矩陣的多個連續(xù)的行或者多個連續(xù)的列。其表示的范圍為從 start 到 end,包含 start,但不包含 end。Range 類的定義如下:
class Range
{
public:
...
int start, end;
};
Range 類還提供了一個靜態(tài)方法 all(),這個方法的作用如同 Matlab 中的“:”, 表示所有的行或者所有的列。
//創(chuàng)建一個單位陣
Mat A = Mat::eye(10, 10, CV_32S);
//提取第 1 到 3 列(不包括 3)
Mat B = A(Range::all(), Range(1, 3));
//提取 B 的第 5 至 9 行(不包括 9)
//其實等價于 C = A(Range(5, 9), Range(1, 3))
Mat C = B(Range(5, 9), Range::all());
圖解如下

7.3 感興趣區(qū)域
從圖像中提取感興趣區(qū)域(Region of interest)有兩種方法,一種是使用構(gòu)造
函數(shù),如下例所示:
/創(chuàng)建寬度為 320,高度為 240 的 3 通道圖像
Mat img(Size(320,240),CV_8UC3);
//roi 是表示 img 中 Rect(10,10,100,100)區(qū)域的對象
Mat roi(img, Rect(10,10,100,100));
除了使用構(gòu)造函數(shù),還可以使用括號運算符,如下:
Mat roi2 = img(Rect(10,10,100,100));
當(dāng)然也可以使用 Range 對象來定義感興趣區(qū)域,如下:
//使用括號運算符
Mat roi3 = img(Range(10,100),Range(10,100));
//使用構(gòu)造函數(shù)
Mat roi4(img, Range(10,100),Range(10,100));
7.4 取對角線元素
矩陣的對角線元素可以使用 Mat 類的 diag()函數(shù)獲取,該函數(shù)的定義如下:
Mat Mat::diag(int d) const
參數(shù) d=0 時,表示取主對角線;當(dāng)參數(shù) d>0 是,表示取主對角線下方的次對 角線,如 d=1 時,表示取主對角線下方,且緊貼主多角線的元素;當(dāng)參數(shù) d<0 時, 表示取主對角線上方的次對角線。
如同 row()和 col()函數(shù),diag()函數(shù)也不進行內(nèi)存復(fù)制操作,其復(fù)雜度也是 O(1)。
測試代碼
///對角線測試
-(void)diag{
Mat grayim(5, 5, CV_8UC1);
for( int i = 0; i < grayim.rows; ++i) {
//獲取第 i 行首像素指針
uchar * p = grayim.ptr<uchar>(i);
//對第 i 行的每個像素(byte)操作
for( int j = 0; j < grayim.cols; ++j )
p[j] = i*grayim.rows+j;
}
cout << "grayim = " << endl << " " << grayim << endl;
Mat diag = grayim.diag();
cout << "diag = " << endl << " " << diag << endl;
Mat diag1 = grayim.diag(-1);
cout << "diag1 = " << endl << " " << diag1 << endl;
Mat diag2 = grayim.diag(1);
cout << "diag2 = " << endl << " " << diag2 << endl;
}
結(jié)果如下
grayim =
[ 0, 1, 2, 3, 4;
5, 6, 7, 8, 9;
10, 11, 12, 13, 14;
15, 16, 17, 18, 19;
20, 21, 22, 23, 24]
diag =
[ 0;
6;
12;
18;
24]
diag1 =
[ 5;
11;
17;
23]
diag2 =
[ 1;
7;
13;
19]
8 Mat 表達式
利用 C++中的運算符重載,OpenCV 2 中引入了 Mat 運算表達式。這一新特 點使得使用 C++進行編程時,就如同寫 Matlab 腳本,代碼變得簡潔易懂,也便于 維護。
如果矩陣 A 和 B 大小相同,則可以使用如下表達式:
C = A + B + 1;
下面給出 Mat 表達式所支持的運算。下面的列表中使用 A 和 B 表示 Mat 類 型的對象,使用 s 表示 Scalar 對象,alpha 表示 double 值。
-
加法,減法,取負(fù):A+B,A-B,A+s,A-s,s+A,s-A,-A -
縮放取值范圍:A*alpha -
矩陣對應(yīng)元素的乘法和除法:A.mul(B),A/B,alpha/A -
矩陣乘法:A*B (注意此處是矩陣乘法,而不是矩陣對應(yīng)元素相乘) -
矩陣轉(zhuǎn)置:A.t() -
矩陣求逆和求偽逆:A.inv() -
矩陣比較運算:A cmpop B,A cmpop alpha,alpha cmpop A。此處 cmpop 可以是>,>=,==,!=,<=,<。如果條件成立,則結(jié)果矩陣(8U 類型矩 陣)的對應(yīng)元素被置為 255;否則置 0。 -
矩陣位邏輯運算:A logicop B,A logicop s,s logicop A,~A,此處 logicop 可以是&,|和^。 -
矩陣對應(yīng)元素的最大值和最小值:min(A, B),min(A, alpha),max(A, B), max(A, alpha)。 -
矩陣中元素的絕對值:abs(A) -
叉積和點積:A.cross(B),A.dot(B)
代碼舉例
-(void)matComputer{
Mat A = Mat::eye(4,4,CV_32SC1);
Mat B = A * 3 + 1;
Mat C = B.diag(0) + B.col(1);
cout << "A = " << A << endl << endl;
cout << "B = " << B << endl << endl;
cout << "C = " << C << endl << endl;
cout << "C .* diag(B) = " << C.dot(B.diag(0)) << endl;
}
測試結(jié)果
A = [1, 0, 0, 0;
0, 1, 0, 0;
0, 0, 1, 0;
0, 0, 0, 1]
B = [4, 1, 1, 1;
1, 4, 1, 1;
1, 1, 4, 1;
1, 1, 1, 4]
C = [5;
8;
5;
5]
C .* diag(B) = 92
9 Mat_類
Mat_類是對 Mat 類的一個包裝,其定義如下:
template<typename _Tp> class Mat_ : public Mat {
public:
//只定義了幾個方法
//沒有定義新的屬性
};
這是一個非常輕量級的包裝,既然已經(jīng)有 Mat 類,為何還要定義一個 Mat_? 下面我們看這段代碼:
Mat M(600, 800, CV_8UC1);
for( int i = 0; i < M.rows; ++i) {
uchar * p = M.ptr<uchar>(i);
for( int j = 0; j < M.cols; ++j ) {
double d1 = (double) ((i+j)%255);
M.at<uchar>(i,j) = d1;
double d2 = M.at<double>(i,j);//此行有錯 }
}
在讀取矩陣元素時,以及獲取矩陣某行的地址時,需要指定數(shù)據(jù)類型。這樣 首先需要不停地寫“<uchar>”,讓人感覺很繁瑣,在繁瑣和煩躁中容易犯錯,如上面代碼中的錯誤,用 at()獲取矩陣元素時錯誤的使用了 double 類型。這種錯誤 不是語法錯誤,因此在編譯時編譯器不會提醒。在程序運行時,at()函數(shù)獲取到 的不是期望的(i,j)位置處的元素,數(shù)據(jù)已經(jīng)越界,但是運行時也未必會報錯。這樣 的錯誤使得你的程序忽而看上去正常,忽而彈出“段錯誤”,特別是在代碼規(guī)模 很大時,難以查錯。
如果使用 Mat_類,那么就可以在變量聲明時確定元素的類型,訪問元素時 不再需要指定元素類型,即使得代碼簡潔,又減少了出錯的可能性。上面代碼可 以用 Mat_實現(xiàn),實現(xiàn)代碼如下面例程里的第二個雙重 for 循環(huán)。
Mat M(600, 800, CV_8UC1);
//在變量聲明時指定矩陣元素類型
Mat_<uchar> M1 = (Mat_<uchar>&)M;
for( int i = 0; i < M1.rows; ++i) {
//不需指定元素類型,語句簡潔
uchar * p = M1.ptr(i);
for( int j = 0; j < M1.cols; ++j ) {
double d1 = (double) ((i+j)%255);
//直接使用 Matlab 風(fēng)格的矩陣元素讀寫,簡潔 M1(i,j) = d1;
double d2 = M1(i,j);
}
}
讀取數(shù)據(jù)能簡單點并且不容易出錯
10 Mat 類的內(nèi)存管理
使用 Mat 類,內(nèi)存管理變得簡單,不再像使用 IplImage 那樣需要自己申請 和釋放內(nèi)存。雖然不了解 Mat 的內(nèi)存管理機制,也無礙于 Mat 類的使用,但是 如果清楚了解 Mat 的內(nèi)存管理,會更清楚一些函數(shù)到底操作了哪些數(shù)據(jù)。
Mat 是一個類,由兩個數(shù)據(jù)部分組成:矩陣頭(包含矩陣尺寸,存儲方法, 存儲地址等信息)和一個指向存儲所有像素值的矩陣的指針,如下圖所示。矩陣頭的尺寸是常數(shù)值,但矩陣本身的尺寸會依圖像的不同而不同,通常比矩陣頭 的尺寸大數(shù)個數(shù)量級。復(fù)制矩陣數(shù)據(jù)往往花費較多時間,因此除非有必要,不要 復(fù)制大的矩陣。
為了解決矩陣數(shù)據(jù)的傳遞,OpenCV 使用了引用計數(shù)機制。其思路是讓每個 Mat 對象有自己的矩陣頭信息,但多個 Mat 對象可以共享同一個矩陣數(shù)據(jù)。讓矩 陣指針指向同一地址而實現(xiàn)這一目的。很多函數(shù)以及很多操作(如函數(shù)參數(shù)傳值) 只復(fù)制矩陣頭信息,而不復(fù)制矩陣數(shù)據(jù)。
前面提到過,有很多中方法創(chuàng)建 Mat 類。如果 Mat 類自己申請數(shù)據(jù)空間, 那么該類會多申請 4 個字節(jié),多出的 4 個字節(jié)存儲數(shù)據(jù)被引用的次數(shù)。引用次數(shù) 存儲于數(shù)據(jù)空間的后面,refcount 指向這個位置,如下圖 所示。當(dāng)計數(shù)等于 0 時,則釋放該空間。

關(guān)于多個矩陣對象共享同一矩陣數(shù)據(jù),我們可以看這個例子:
Mat A(100,100, CV_8UC1);
Mat B = A;
Mat C = A(Rect(50,50,30,30));
上面代碼中有三個 Mat 對象,分別是 A,B 和 C。這三者共有同一矩陣數(shù)據(jù), 其示意圖如下圖所示。
