本文为对《OpenCV计算机视觉编程攻略(第二版)》4.8节积分图像部分的学习笔记。
直方图的计算方法为遍历图像的全部像素并累计每个强度值在图像中出现的次数。有时仅需要计算图像中某个特定区域的直方图,而如果需要计算图像中多个区域的直方图,这些计算过程将变得非常耗时。在这种情况下使用积分图像将极大地提高统计图像子区域像素的效率。积分图像在程序中应用非常广泛。
积分图像的定义:取图像左上侧的全部像素计算累加和,并用这个累加和替换图像中的每一个像素,使用这种方式得到的图像称为积分图像。
计算积分图像的函数:C++接口
函数参数:
根据定义,计算源图像的积分图像的函数执行如下操作:
按照上面的公式,P0点的积分值为蓝色方框中像素的强度累加和,P3点的积分像素为黄色方框内像素点的强度累加和。从上图可知,想要从原始图像获得积分图像,只需要对整幅图像扫描一次。这是因为对同一行(列)的相邻两像素,当前像素的积分值等于上一像素的积分值加上当前像素所在列(行)的左上侧累计值。可见,积分图像是一个包含像素累加和的新图像。为了防止累加和过大溢出,积分图像的值通常都使用int类型变量(CV_32S)或float类型变量(CV_32F)。
对于多通道图像,每个通道都将独立累积计算积分图像。
假设一幅图像中4个点ABCD,其积分图像中A(x1,y1)点的值为其左上侧所有像素的值的累加和,也就是蓝色区域中所有像素点的值累加和。同理积分图像中的B(x2,y1)、C(x1,y2)、D(x2,y2)点值分别是绿色、紫色和黄色区域像素值的累加和。ABCD四点的位置关系在右下角图所示。
那么如果需要计算由ABCD组成的ROI的累加值就只需要使用如下公式:
即:D-C-B+A。显然,计算量不受区域尺寸的影响。所以,如果需要在多个尺寸的区域上计算像素累加和,最好采用积分图像。
局部阈值,即根据每个像素的邻域计算阈值。这种策略叫做自适应阈值化,包括将每个像素的值与邻域的平均值进行比较。如果某像素的值与它的局部平均值差别很大,就会被当做异常值在阈值化过程中剔除。因此,自适应阈值化需要计算每个像素周围的局部平均值。这需要多次计算图像窗口的累计值,可以通过积分图像来提高计算效率。
首先,读取一幅图片
cv::Mat image= cv::imread("f:\\images\\book.jpg",0); if (!image.data) return 0; // rotate the image for easier display cv::transpose(image, image); cv::flip(image, image, 0); // display original image cv::namedWindow("Original Image"); cv::imshow("Original Image",image);
原始图像
观察一下使用固定阈值分割得到的结果
// using a fixed threshold cv::Mat binaryFixed; cv::Mat binaryAdaptive; cv::threshold(image,binaryFixed,180,255,cv::THRESH_BINARY);//固定阈值分割 cv::namedWindow("Fixed Threshold"); cv::imshow("Fixed Threshold",binaryFixed);
使用固定阈值分割的结果
显然,固定阈值难以完成这个任务,难以给出不同光照区域的最佳分割。对于这类问题,可以使用自适应阈值分割。实现自适应阈值分割有很多方法,最简单的就是直接使用opencv中的自适应阈值分割函数。
// using as adaptive threshold int blockSize= 21; // size of the neighborhood int threshold=10; // pixel will be compared to (mean-threshold) cv::adaptiveThreshold(image, // input image binaryAdaptive, // output binary image 255, // max value for output cv::ADAPTIVE_THRESH_MEAN_C, // adaptive method方法 cv::THRESH_BINARY, // threshold type blockSize, // size of the block threshold); // threshold used cv::namedWindow("Adaptive Threshold"); cv::imshow("Adaptive Threshold",binaryAdaptive);
自适应阈值函数cv::AdaptiveThreshold的分割结果
// compute integral image IntegralImage<int,1> integral(image);//IntegralImage为计算积分图像的方法封装的模板类,实例化一个变量 cv::Mat binary= image.clone();//深度复制图像 // 使用指针遍历图像来计算积分图像 int nl= binary.rows; // number of lines int nc= binary.cols; // total number of elements per line // compute integral image cv::Mat iimage; cv::integral(image,iimage,CV_32S);//计算积分图像 // for each row int halfSize= blockSize/2; <span style="white-space:pre"> </span>for (int j=halfSize; j<nl-halfSize-1; j++) { // get the address of row j uchar* data= binary.ptr<uchar>(j); int* idata1= iimage.ptr<int>(j-halfSize); int* idata2= iimage.ptr<int>(j+halfSize+1); // for pixel of a line for (int i=halfSize; i<nc-halfSize-1; i++) { // compute sum int sum= (idata2[i+halfSize+1]-idata2[i-halfSize]- idata1[i+halfSize+1]+idata1[i-halfSize])/(blockSize*blockSize); // apply adaptive threshold if (data[i]<(sum-threshold)) data[i]= 0; else data[i]=255; } } cv::namedWindow("Adaptive Threshold (integral)"); cv::imshow("Adaptive Threshold (integral)",binary);
在积分图像上实现自适应阈值分割
用积分图像实现自适应阈值分割的结果与使用自适应阈值分割函数AdaptiveThreshold()完全一样。上段代码中的IntegralImage类,在integral.h头文件中定义,该步骤将积分计算的过程封装进了模板类中。
#if !defined IINTEGRAL #define IINTEGRAL #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <vector> template <typename T, int N> //将积分计算过程封装进模板类<T>为向量数据类型,<N>为向量数量 class IntegralImage { cv::Mat integralImage; public: IntegralImage(cv::Mat image) { // (costly) computation of the integral image cv::integral(image,integralImage,cv::DataType<T>::type); } // compute sum over sub-regions of any size from 4 pixel access cv::Vec<T,N> operator()(int xo, int yo, int width, int height) { // window at (xo,yo) of size width by height return (integralImage.at<cv::Vec<T,N> >(yo+height,xo+width)//Vec<T,N> ,T为类型,N为向量元素数量 -integralImage.at<cv::Vec<T,N> >(yo+height,xo) -integralImage.at<cv::Vec<T,N> >(yo,xo+width) +integralImage.at<cv::Vec<T,N> >(yo,xo)); } // compute sum over sub-regions of any size from 4 pixel access cv::Vec<T,N> operator()(int x, int y, int radius) { // square window centered at (x,y) of size 2*radius+1 return (integralImage.at<cv::Vec<T,N> >(y+radius+1,x+radius+1) -integralImage.at<cv::Vec<T,N> >(y+radius+1,x-radius) -integralImage.at<cv::Vec<T,N> >(y-radius,x+radius+1) +integralImage.at<cv::Vec<T,N> >(y-radius,x-radius)); } }; // convert to a multi-channel image made of binary planes // nPlanes must be a power of 2 void convertToBinaryPlanes(const cv::Mat& input, cv::Mat& output, int nPlanes) { // number of bits to mask out int n= 8-static_cast<int>(log(static_cast<double>(nPlanes))/log(2.0)); // mask used to eliminate least significant bits uchar mask= 0xFF<<n; // e.g. for div=16, mask= 0xF0 // create a vector of 16 binary images std::vector<cv::Mat> planes; // reduce to nBins bins by eliminating least significant bits cv::Mat reduced= input&mask; // compute each binary image plane for (int i=0; i<nPlanes; i++) { // 1 for each pixel equals to i<<shift planes.push_back((reduced==(i<<n))&0x1); } // create multi-channel image cv::merge(planes,output); } #endif
// adaptive threshold using image operators cv::Mat filtered; cv::Mat binaryFiltered; cv::boxFilter(image,filtered,CV_8U,cv::Size(blockSize,blockSize)); filtered= filtered-threshold; binaryFiltered= image>= filtered;通过实验,上述3中方法中,使用第一种自适应阈值化函数花费的时间较少。