OpenCV 之ios 圖像的基本操作

1 圖像的表示

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


1-1
1-2

一般來說,灰度圖2維矩陣表示,彩色(多通道)圖像3 維矩陣(M × N × 3)表示。對于圖像顯示來說,目前大部分設(shè)備都是用無符號 8 位整 數(shù)(類型為 CV_8U)表示像素亮度。

圖像數(shù)據(jù)在計算機內(nèi)存中的存儲順序為以圖像最左上點(也可能是最左下 點)開始,存儲如表1-1 所示。


表1-1 灰度圖像的存儲示意圖

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

表1-2彩色 RGB 圖像的存儲示意圖

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ù)遍歷圖像的例程的輸出結(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é)果如下


使用迭代器遍歷圖像的例程的輸出結(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é)果如圖


使用指針遍歷圖像的例程的輸出結(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 時,則釋放該空間。


Mat 類中的數(shù)據(jù)存儲示意圖,refcount 變量指向數(shù)據(jù)區(qū)后面,用 4 個字節(jié)(int 類型) 存儲引用數(shù)目

關(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ù), 其示意圖如下圖所示。


三個矩陣頭共用共用同一矩陣數(shù)據(jù)

github地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容