欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

OpenCV(六) Opencv中 core 核心模块详解——访问图像像素的几种方法

程序员文章站 2022-05-16 10:35:44
...

      介绍图像,当然要介绍如何访问图像的像素了~~

      1、像素在内存中的存储方式

      我们知道,图像是由像素组成。对于一幅图片,我们看到的是场景、颜色等,但是计算机看到的是一个大矩阵,矩阵包含的数据成千上万。那么,图像中的像素在计算机内存中是如何存储的呢?

      对于一幅灰度图像,即黑白图片(每个像素都有一个采样颜色,为单通道图像),每个像素值  Mat(i,j) 就是一个灰度值。如下图所示。灰度值一般范围为 0~255,白色为 255 ,黑色为0。

OpenCV(六) Opencv中 core 核心模块详解——访问图像像素的几种方法

      但是彩色图像每个像素是有分量的。什么意思呢?以 RGB 图像为例,图像具有三通道,即每个像素都是由色彩分量 R、G、B组成,也就是说,每个像素都是由三个值组成的,但是最后反映到像素上,却是一个值。需要注意的是,第一个通道是 B 蓝色分量,第三个通道是 R 分量

OpenCV(六) Opencv中 core 核心模块详解——访问图像像素的几种方法

      通常情况下,uchar 类型的三通道图像,RGB各有256级亮度,用数字表示为从0、1、2...直到255。按照计算,256级的RGB色彩总共能组合出约1678万种色彩,即256×256×256=16777216。通常也被简称为1600万色或千万色。也称为24位色(2的24次方)。

      2、颜色空间缩减

      我们知道,如果是 uchar 类型的三通道图像,存储像素颜色就有255 * 255 *255 = 1600万 多个,这大大增加了计算的复杂度,对算法性能的影响非常大。因此我们可以采用颜色空间缩减。具体做法就是:将颜色空间值除以某个值,来获得较少的颜色数。举个例子,0~10,我取0,10 ~19我取10,,依次类推吧。如果这样的话,就将颜色的取值减少为 26*26 *26。有人说,这样不就降低图像的质量了吗?对,没错,效率和质量有时候是很难共存的。

      我们可以利用 int  处罚截余达到我们的目的。代码如下:

uchar table[256];
int n = 10;
for(int i = 0;i < 256;i++)
        {
                table[i] = n * ( i/n);
        }

      利用这种查表法,预先计算所有的可能值,只需要读取,不需要计算,可以大大较少图像的运算。对于此类操作,OpenCV 推荐使用 LUT函数:Look Up Table。程序如下:

Mat lookUpTable(1,256,CV_8U);
uchar *p = lookUpTable.date;
for(int i = 0; i < 256;++i)
        {
                p[i] = table[i];
        }
for(int i = 0;i < times;i++)
        LUT(I, lookUpTable,J);//I 是输入,J 是输出

       3、遍历像素的方法

      很显然,如果仅仅利用一个for()循环,把一张图像的每一行、每一列都遍历一遍,显然是非常愚蠢的行为。因为效率会非常非常低!在 OpenCV中,提供了三种方式进行像素的遍历,具体来看一下。

      1)动态地址法:利用下标 M.at<cv::Vec3b>(i,j)

      先说一下 Mat::at()函数是个什么鬼引用。at() 返回一个数组元素的!image.at<cv::Vec3b>(i,j)[k] 表示的是取出图像image 第 i 行 j 列第 k 个通道的颜色点。其中cv::Vec3b 是图像像素值类型,其函数模板是 typedef Vec<uchar,3>Vec3b,表示3通道 uchar 类型。

      直接上程序:

int main()
{
	Mat img = imread("test.jpg");
       //动态地址法
        Mat img1 = img.clone();
        int rows1 = img1.rows;
        int cols1 = img1.cols;
        
        for(int i = 0;i < rows1;i++)
          {
            for(int j = 0;j < cols1;j++)
                {
                    img1.at<Vec3b>(i,j)[0] = 255-img1.at<Vec3b>(i,j)[0];
                    img1.at<Vec3b>(i,j)[1] = 255-img1.at<Vec3b>(i,j)[1];
                    img1.at<Vec3b>(i,j)[2] = 255-img1.at<Vec3b>(i,j)[2];
                }
           }     
      imshow("变换后的图像",img1);
      imwrite("mn.jpg",img1);
      waitKey();
      return 0;
}
  这个程序实现的是对对每一个像素取反。最终得到的图像是这样的(第一幅为原图,第二个为变换后的图),感觉变帅了点啊哈哈哈

OpenCV(六) Opencv中 core 核心模块详解——访问图像像素的几种方法          OpenCV(六) Opencv中 core 核心模块详解——访问图像像素的几种方法

      2)指针遍历法:利用 Mat :: ptr<type>

      直接上程序

   //指针遍历方法
      int cols2 = cols1 * img1.channels();  //因为是三通道的,所以,元素数是行数的3倍
      for(int i = 0;i < rows1;i++)
          {
              uchar *date = img1.ptr<uchar>(i);
              for(int j = 0;j < cols2;j++)
                  {
                      date[j] = 255 - date[j];
                  }
          }
      imshow("mndeng.jpg",img1);   
      waitKey();
      return 0;    
}
  得到的结果和原来是一样的。这里强调一下,彩色图像都是有通道数的,因此,行元素的个数是图像行数 × 通道数

      3)使用迭代器

  这个方法不赘述,迭代器是 C++11 标准里面才有的,我也不喜欢用,算法又耗费时间。所以,在这里就不说了。

  那么,哪种方法遍历像素最节省时间呢,当然是指针法了。接下来,咱们用计时函数测试一下。OpenCV 中提供了两个计时函数 getTickCount() 和 getTickFrequency()函数,  getTickCount() 函数是返回CPU自某个事件以来时钟周期数(即周期T);getTickFrequency()函数是返回CPU一秒钟走的时钟周期数(即频率),我们把这两个函数结合起来使用,就可以记录运行时间。用法如下:

double time = static_cast<double>(getTickCount());
*************开始图像处理 *****************

time = ((double)getTickCount() - time)/getTickFrequency();
  cout << "运行时间" << time << endl;

      完整的程序如下:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#include <vector>
using namespace std;
using namespace cv;


int main()
{
	Mat img = imread("test.jpg");
        Mat img1 = img.clone();
        int rows1 = img1.rows;
        int cols1 = img1.cols;
        
        double time = static_cast<double>(getTickCount());
         //动态地址法
        for(int i = 0;i < rows1;i++)
          {
            for(int j = 0;j < cols1;j++)
                {
                    img1.at<Vec3b>(i,j)[0] = 255-img1.at<Vec3b>(i,j)[0];
                    img1.at<Vec3b>(i,j)[1] = 255-img1.at<Vec3b>(i,j)[1];
                    img1.at<Vec3b>(i,j)[2] = 255-img1.at<Vec3b>(i,j)[2];
                }
           }  
       time = ((double)getTickCount() - time)/getTickFrequency();
       cout << "动态地址法遍历像素运行时间" << time << endl; //0.0319265 s
     //  imshow("变换后的图像",img1);
     //  imwrite("mn.jpg",img1);
      
 /*      //指针遍历方法
      int cols2 = cols1 * img1.channels();  //因为是三通道的,所以,元素数是行数的3倍
      for(int i = 0;i < rows1;i++)
          {
              uchar *date = img1.ptr<uchar>(i);
              for(int j = 0;j < cols2;j++)
                  {
                      date[j] = 255 - date[j];
                  }
          }
      time = ((double)getTickCount() - time)/getTickFrequency();
      cout << "指针法遍历像素运行时间" << time << endl;  // 0.00685272 s
      imshow("mndeng.jpg",img1);   
 */
      waitKey();
      return 0;    
}
      从实验数据可以看出来,利用指针法遍历像素,效率非常高。建议大家使用!