这里讨论了以下几个主题:

  • 访问像素的几种方法
  • 图像融合
  • copyMakeBorder函数
  • Sobel、Laplace、Canny边缘提取
  • 绘制图像的直方图
  • 反向投影
  • DFT变换
  • DCT变换
  • OpenCV内置的XML,YAML文件访问机制


首先打印一些Mat的信息吧~  :)

void ShowImageInfo()
{
	cout << image.step << endl
		 << image.rows << " * " << image.cols << endl
		 << image.size().height << " * " << image.size().width << endl
		 << image.elemSize() << endl
		 << image.channels() << endl
		 << image.total() << endl
		 << boolalpha << image.isContinuous() << endl;
}




访问像素的几种方法

  • SimplyAccess使用at成员函数来进行像素访问,简单但是不好用,因为速度很慢!
  • IteratorAccess适合像素便利,但速度也很慢!
  • FastestPointAccess才是真正实用的像素访问方法,尤其是当continuous时把行设置为1,列设置为rows*cols的算法,这样非常高效且好用,应该只使用这一种像素访问方式。
void SimplyAccess()
{
	for (int i = 0; i < image.rows; ++i)
	{
		for (int j = 0; j < image.cols; ++j)
		{
			if (rand() % 5 == 0)
			{
				image.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
			}
		}
	}
	imshow("img", image);

	Mat_<Vec3b> im2 = image;
	for (int i = 0; i < image.rows; ++i)
	{
		for (int j = 0; j < image.cols; ++j)
		{
			if (rand() % 5 == 0)
			{
				im2(i, j) = Vec3b(0, 0, 0);
			}
		}
	}
	imshow("im2", im2);

	waitKey();
}

void IteratorAccess()
{
	auto it = image.begin<Vec3b>();
	auto iter_end = image.end<Vec3b>();
	for (; it != iter_end; ++it)
	{
		if (rand() % 3 == 0)
		{
			(*it) = Vec3b(255, 255, 255);
		}
	}
	imshow("image", image);
	waitKey();
}

void FastestPointAccess()
{
	int div = 64;
	int nl = image.rows; // number of lines
	int nc = image.cols * image.channels();

	// 如果是continuous,就可以把图片视为1*(rows*cols)的单行图片,这种方法太帅了!
	// 对于Continuous的图片,后面就只需要进行一次ptr函数取地址就可以了,剩下的全是指针运算,太高效了!
	if (image.isContinuous())
	{
		// then no padded pixels
		nc = nc * nl;
		nl = 1;  // it is now a 1D array
	}

	// this loop is executed only once
	// in case of continuous images
	for (int j = 0; j < nl; j++)
	{
		uchar* data = image.ptr<uchar>(j);
		for (int i = 0; i < nc; i++)
		{
			// process each pixel ---------------------
			data[i] = data[i] / div * div + div / 2;
			// end of pixel processing ---------------- 
		}
	}
	imshow("image", image);
	waitKey();
}



图像融合

图像融合是指把两幅图像渐近的融合在一起,不用多废话,聪明的你一看就一切明白了。

代码如下:

int main( int argc, char** argv )
{
  cv::Mat image = cv::imread("/home/chuanqi/ImageDataset/wy.jpg");
  cv::Mat image2 = cv::imread("/home/chuanqi/ImageDataset/111.jpg");

  cv::resize(image2, image2, image.size());
  cv::Mat result;
  cv::addWeighted(image, 0.5, image2, 0.5, 0., result);
  imshow("result", result);

  waitKey();
  return 0;
}

效果如下:



copyMakeBorder

copyMakeBorder是个很常用的函数,用来给图像添加边框添,加边框的操作在很多的图像处理中都需要用到。使用下面的代码来进行copyMakeBorder函数的测试:

int main() {
  Mat src = imread("/home/chuanqi/ImageDataset/wy.jpg");
  imshow("src", src);
  Mat dst1, dst2;

  int const k_border_size = 20;
  copyMakeBorder(src, dst1,
                 k_border_size, k_border_size,
                 k_border_size, k_border_size,
                 cv::BORDER_CONSTANT, Scalar(0, 0, 255));
  copyMakeBorder(src, dst2,
                 k_border_size, k_border_size,
                 k_border_size, k_border_size,
                 cv::BORDER_REPLICATE);

  imshow("dst1", dst1);
  imshow("dst2", dst2);
  waitKey();

  return 0;
}

效果如下图所示:




Sobel、Laplace、Canny边缘提取

采用的测试代码如下:

int main() {
  Mat src = imread("/home/chuanqi/ImageDataset/wy.jpg");
  if( !src.data )
  { return -1; }

  imshow("src", src);
  char const *window_name = "Sobel Demo - Simple Edge Detector";
  namedWindow( window_name, CV_WINDOW_AUTOSIZE );
  int scale = 1;
  int delta = 0;
  int ddepth = CV_16S;
  GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT );

  /// 转换为灰度图
  Mat src_gray;
  cvtColor( src, src_gray, CV_RGB2GRAY );

  /// 创建 grad_x 和 grad_y 矩阵
  Mat grad_x, grad_y;
  /// 求 X方向梯度
  //Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT );
  Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT );
  convertScaleAbs( grad_x, grad_x );
  /// 求Y方向梯度
  //Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT );
  Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );
  convertScaleAbs( grad_y, grad_y );
  /// 合并梯度(近似)
  Mat grad;
  addWeighted( grad_x, 0.5, grad_y, 0.5, 0, grad );
  imshow( window_name, grad );

  /// 同时计算x,y方向的Sobel
  Mat grad_xy;
  Sobel(src_gray, grad_xy, ddepth, 1, 1, 3, scale, delta, BORDER_DEFAULT);
  convertScaleAbs(grad_xy, grad_xy);
  grad_xy *= 10;
  imshow("grad_xy", grad_xy);

  /// 计算Laplace算子的结果
  Mat laplace_dst;
  Laplacian(src, laplace_dst, CV_16S, 3);
  convertScaleAbs(laplace_dst, laplace_dst);
  imshow("laplace_dst", laplace_dst);

  /// 计算Canny的结果
  Mat canny_dst;
  Canny(src, canny_dst, 100, 200);
  imshow("canny_dst", canny_dst);

  waitKey(0);
  return 0;
}

效果如下图所示:



绘制图像的直方图

画一幅3通道的图像的直方图,先split为3个通道,然后对各个通道进行直方图计算,并以不同的颜色画出来

Mat GetHistogramImage(Mat const &src) {
  if (src.empty()) {
    return Mat();
  }

  /// 分割成3个单通道图像 ( R, G 和 B )
  vector<Mat> rgb_planes;
  split( src, rgb_planes );

  /// 设定bin数目
  int histSize = 255;

  /// 设定取值范围 ( R,G,B) )
  float range[] = { 0, 255 } ;
  const float* histRange = { range };

  bool uniform = true; bool accumulate = false;

  Mat r_hist, g_hist, b_hist;

  /// 计算直方图:
  calcHist( &rgb_planes[0], 1, 0, Mat(), r_hist, 1, &histSize, &histRange, uniform, accumulate );
  calcHist( &rgb_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRange, uniform, accumulate );
  calcHist( &rgb_planes[2], 1, 0, Mat(), b_hist, 1, &histSize, &histRange, uniform, accumulate );

  // 创建直方图画布
  int hist_w = 255; int hist_h = 255;
  int bin_w = cvRound( (double) hist_w/histSize );

  Mat histImage( hist_w, hist_h, CV_8UC3, Scalar( 0,0,0) );

  /// 将直方图归一化到范围 [ 0, histImage.rows ]
  normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
  normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
  normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );

  /// 在直方图画布上画出直方图
  for( int i = 1; i < histSize; i++ ) {
    line( histImage, Point( bin_w*(i-1), hist_h - cvRound(r_hist.at<float>(i-1)) ) ,
                     Point( bin_w*(i), hist_h - cvRound(r_hist.at<float>(i)) ),
                     Scalar( 0, 0, 255), 2, 8, 0  );
    line( histImage, Point( bin_w*(i-1), hist_h - cvRound(g_hist.at<float>(i-1)) ) ,
                     Point( bin_w*(i), hist_h - cvRound(g_hist.at<float>(i)) ),
                     Scalar( 0, 255, 0), 2, 8, 0  );
    line( histImage, Point( bin_w*(i-1), hist_h - cvRound(b_hist.at<float>(i-1)) ) ,
                     Point( bin_w*(i), hist_h - cvRound(b_hist.at<float>(i)) ),
                     Scalar( 255, 0, 0), 2, 8, 0  );
   }
  return histImage;
}

int main() {
  Mat src = imread("/home/chuanqi/ImageDataset/wy.jpg");

  /// 显示直方图
  namedWindow("calcHist Demo", CV_WINDOW_AUTOSIZE );
  imshow("calcHist Demo", GetHistogramImage(src));

  waitKey(0);
  return 0;
}

效果如下图所示:


反向投影

反向投影其实就是做了一个Look Up Table查找而已,简要描述如下:
1:首先要提供一个目标直方图和一个测试图像;
2:对测试图像中的每一个像素,去目标直方图中查找该像素的值所对应的bin的值;
3:这个值就是最后的结果图像中的灰度值,非常简单易懂!

测试代码如下:

int main( int argc, char** argv )
{
  Mat src = imread("/home/chuanqi/ImageDataset/wy.jpg", 0);

  /// 计算直方图并归一化
  MatND hist;
  int histSize = 100;
  float hue_range[] = { 0, 180 };
  const float* ranges = { hue_range };
  calcHist( &src, 1, 0, Mat(), hist, 1, &histSize, &ranges, true, false );
  normalize( hist, hist, 0, 255, NORM_MINMAX, -1, Mat() );

  /// 计算反向投影
  MatND backproj;
  calcBackProject( &src, 1, 0, hist, backproj, &ranges, 1, true );

  imshow( "BackProj", backproj );
  waitKey(0);
  return 0;
}

效果图如下:



DFT变换

首先进行DFT变换,再进行IDFT变换回原来的图像;以及,只取DFT变换结果的前16个系数,就可以大致的表示图像的大体轮廓了。

首先看代码:

int main( int argc, char** argv )
{
  struct {
    void operator()(Mat &src, Mat& dst){
      Mat tmp;
      int cx = src.cols/2;
      int cy = src.rows/2;

      for(int i=0; i<=cx; i+=cx) {
        Mat qs(src, Rect(i^cx,0,cx,cy));
        Mat qd(dst, Rect(i,cy,cx,cy));
        qs.copyTo(tmp);
        qd.copyTo(qs);
        tmp.copyTo(qd);
      }
    }
  } shift_dft;

  cv::Mat src_img = cv::imread("/home/chuanqi/ImageDataset/wy.jpg", 0);
  imshow("src", src_img);
  Size s_size = src_img.size();
  int src_cols = s_size.width;
  int src_rows = s_size.height;
  Mat Re_img = Mat(s_size, CV_64F);
  Mat Im_img = Mat::zeros(s_size, CV_64F);
  Mat Complex_img = Mat(s_size, CV_64FC2);

  src_img.convertTo(Re_img, CV_64F);
  vector<Mat> mv;
  mv.push_back(Re_img);
  mv.push_back(Im_img);
  merge(mv, Complex_img);
  Mat zero;
  int dft_rows = getOptimalDFTSize(src_rows);
  int dft_cols = getOptimalDFTSize(src_cols);
  Mat dft_src = Mat::zeros(dft_rows, dft_cols, CV_64FC2);

  Mat roi(dft_src, Rect(0, 0, src_cols, src_rows));
  Complex_img.copyTo(roi);

  Mat dft_dst;
  dft(dft_src, dft_dst);

  //split(dft_dst.mul(dft_dst), mv);
  //sqrt(mv[0]+mv[1], mv[0]);
  split(dft_dst, mv);
  magnitude(mv[0], mv[1], mv[0]);
  log(mv[0]+1, mv[0]); // for ver. 2.1 or later

  double dft_coefs[64];
  for (int i = 0; i < 8; ++i) {
    for (int j = 0; j < 8; ++j) {
      dft_coefs[i*8+j]=mv[0].at<double>(i,j);
    }
  }

  shift_dft(mv[0], mv[0]);


  Mat mag_img;
  normalize(mv[0], mag_img, 0, 1, CV_MINMAX);
  imshow("dft", mag_img);


  double min, max;
  idft(dft_dst, dft_src);
  split(dft_src, mv);
  minMaxLoc(mv[0], &min, &max);
  Mat idft_img1 = Mat(mv[0]*1.0/max, Rect(0, 0, src_cols, src_rows));
  imshow("idft1", idft_img1);

  cv::Point center(dft_dst.rows / 2, dft_dst.cols / 2);
  for (int i = 0; i < dft_dst.rows; ++i) {
    for (int j = 0; j < dft_dst.cols; ++j) {
      if (i>4 && j>4) {
        dft_dst.at<Vec2d>(i, j) = Vec2d(0, 0);
      }
    }
  }

  idft(dft_dst, dft_src);
  split(dft_src, mv);
  minMaxLoc(mv[0], &min, &max);
  Mat idft_img2 = Mat(mv[0]*1.0/max, Rect(0, 0, src_cols, src_rows));
  imshow("idft2", idft_img2);

  waitKey();
  return 0;
}

效果如下图所示:



DCT变换

与DFT类似的常用变换还有DCT变换,与DFT变换非常类似,但是更简单一些,我更喜欢用:)

代码如下:

int main( int argc, char** argv )
{
  cv::Mat img = cv::imread("/home/chuanqi/ImageDataset/wy.jpg", 0);
  imshow("src", img);
  cv::Mat src = cv::Mat_<double>(img);
  cv::Size even_size(src.cols % 2 == 0 ? src.cols : src.cols - 1, src.rows % 2 == 0 ? src.rows : src.rows - 1);
  cv::resize(src, src, even_size);

  cv::Mat dst;
  cv::dct(src, dst);
  cv::imshow("dst", dst);

  cv::Mat dsti1;
  cv::idct(dst, dsti1);
  cv::Mat idst1 = cv::Mat_<uchar>(dsti1);
  cv::imshow("idst1", idst1);

  for (int i = 0; i < dst.rows; ++i) {
    for (int j = 0; j < dst.cols; ++j) {
      if (i > dst.rows / 4 || j > dst.cols / 4) {
        dst.at<double>(i, j) = 0;
      }
    }
  }
  cv::Mat dsti2;
  cv::idct(dst, dsti2);
  cv::Mat idst2 = cv::Mat_<uchar>(dsti2);
  cv::imshow("idst2", idst2);

  cv::waitKey();
  return 0;
}

效果如下图所示:



OpenCV内置的XML,YAML文件访问机制

OpenCV中内置的XML、YAML文件访问机制非常非常强大啊,我觉得对于一般的应用已经没有必要再去单独使用特别的XML库了,内置的这个API太好用了。

支持sequence和map,感觉风格有点像python。废话不多说,继续看代码:

void XmlYamlTest(string const &path) {
	FileStorage fs(path, FileStorage::WRITE);
	fs << "iterationNr" << 100;
	fs << "strings" << "["; // text - string sequence
	fs << "image1.jpg" << "Awesomeness" << "baboon.jpg";
	fs << "]"; // close sequence
	fs << "Mapping"; // text - mapping
	fs << "{" << "One" << 1;
	fs << "Two" << 2 << "}";
	Mat R(5, 6, CV_8UC1);
	fs << "R" << R; // cv::Mat
	fs.release(); // explicit close
}

int main(int argc, char* const argv[])
{
	XmlYamlTest("/home/chuanqi/1.txt");
	XmlYamlTest("/home/chuanqi/1.xml");
	cout << "Write Done." << endl;
	return 0;
}

生成的两个文件如下所示,可以看出,OpenCV根据后缀名来决定使用XML还是YAML格式,对于XML后缀的就是XML格式,其它的情况都默认使用YAML:

==========  1.txt  ==========
%YAML:1.0
 iterationNr: 100
 strings:
    - "image1.jpg"
    - Awesomeness
    - "baboon.jpg"
 Mapping:
    One: 1
    Two: 2
 R: !!opencv-matrix
    rows: 5
    cols: 6
    dt: u
    data: [ 255, 255, 255, 255, 0, 0, 0, 0, 184, 195, 138, 1, 0, 0, 0, 0,
        88, 227, 138, 1, 0, 0, 0, 0, 0, 0, 255, 66, 0, 0 ]==========  1.xml  ==========
<?xml version="1.0"?>
 <opencv_storage>
 <iterationNr>100</iterationNr>
 <strings>
   image1.jpg Awesomeness baboon.jpg</strings>
 <Mapping>
   <One>1</One>
   <Two>2</Two></Mapping>
 <R type_id="opencv-matrix">
   <rows>5</rows>
   <cols>6</cols>
   <dt>u</dt>
   <data>
     255 255 255 255 0 0 0 0 184 195 138 1 0 0 0 0 88 227 138 1 0 0 0 0 0
     0 255 66 0 0</data></R>
 </opencv_storage>