阅读opencv计算机视觉编程三(直方图统计像素一)

it2023-02-18  90

直方图统计像素:  计算图像直方图;  利用查找表修改图像外观;  直方图均衡化;  反向投影直方图检测特定图像内容;  用均值平移算法查找目标;  比较直方图搜索相似图像;  用积分图像统计像素

计算图像直方图: 直方图是一个简单的表格,表示一幅图像(有时是一组图像)中具有某个值的像素的数量。 因此,灰度图像的直方图有256 个项目,也叫箱子(bin) OpenCV 中计算直方图,可简单地调用cv::calcHist 函数。这是一个通用的直方图 计算函数,可处理包含任何值类型和范围的多通道图像。为了简化,这里指定一个专门用于处理 单通道灰度图像的类。cv::calcHist 函数非常灵活,在处理其他类型的图像时都可以直接使用它。 这个专用类的初始化代码为: // 创建灰度图像的直方图

class Histogram1D { private: int histSize[1]; // 直方图中箱子的数量 float hranges[2]; // 值范围 const float* ranges[1]; // 值范围的指针 int channels[1]; // 要检查的通道数量 public: Histogram1D() { // 准备一维直方图的默认参数 histSize[0]= 256; // 256 个箱子 hranges[0]= 0.0; // 从0 开始(含) hranges[1]= 256.0; // 到256(不含) ranges[0]= hranges; channels[0]= 0; // 先关注通道0 }

定义好成员变量后,就可以用下面的方法计算灰度直方图了: // 计算一维直方图

cv::Mat getHistogram(const cv::Mat &image) { cv::Mat hist; // 用calcHist 函数计算一维直方图 cv::calcHist(&image, 1, // 仅为一幅图像的直方图 channels, // 使用的通道 cv::Mat(), // 不使用掩码 hist, // 作为结果的直方图 1, // 这是一维的直方图 histSize, // 箱子数量 ranges // 像素值的范围 ); return hist; }

程序只需要打开一幅图像,创建一个Histogram1D 实例,然后调用getHistogram 方法 即可: // 读取输入的图像

cv::Mat image= cv::imread("group.jpg", 0); // 以黑白方式打开 // 直方图对象 Histogram1D h; // 计算直方图 cv::Mat histo= h.getHistogram(image); 这里的histo 对象是一个一维数组,包含256 个项目。因此只需遍历这个数组,就可以读 取每个箱子: // 循环遍历每个箱子 for (int i=0; i<256; i++) cout << "Value " << i << " = " <<histo.at<float>(i) << endl;

比较实用的做法是以函数的方式 显示直方图,例如用柱状图。用下面这几种方法可创建这种图形: // 计算一维直方图,并返回它的图像

cv::Mat getHistogramImage(const cv::Mat &image, int zoom=1) { // 先计算直方图 cv::Mat hist= getHistogram(image); // 创建图像 return getImageOfHistogram(hist, zoom); }

// 创建一个表示直方图的图像(静态方法)

static cv::Mat getImageOfHistogram (const cv::Mat &hist, int zoom) { // 取得箱子值的最大值和最小值 double maxVal = 0; double minVal = 0; cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0); // 取得直方图的大小 int histSize = hist.rows; // 用于显示直方图的方形图像 cv::Mat histImg(histSize*zoom, histSize*zoom, CV_8U, cv::Scalar(255)); // 设置最高点为90%(即图像高度)的箱子个数 int hpt = static_cast<int>(0.9*histSize); // 为每个箱子画垂直线 for (int h = 0; h < histSize; h++) { float binVal = hist.at<float>(h); if (binVal>0) { int intensity = static_cast<int>(binVal*hpt / maxVal); cv::line(histImg, cv::Point(h*zoom, histSize*zoom), cv::Point(h*zoom, (histSize - intensity)*zoom), cv::Scalar(0), zoom); } } return histImg; }

使用getImageOfHistogram 方法可以得到直方图图像。它用线条画成,以柱状图形式 展现: // 以图像形式显示直方图

cv::namedWindow("Histogram"); cv::imshow("Histogram",h.getHistogramImage(image));

从上面图形化的直方图可以看出,在中等灰度值处有一个大的尖峰,并且比中等值更黑的像 素有很多。巧的是,这两部分像素分别对应了图像的背景和前景。要验证这点,可以在这两部分 的汇合处进行阈值化处理。OpenCV 中的cv::threshold 函数可以实现这个功能。上一章介绍 过,它是一个很实用的函数。我们取直方图中在升高为尖峰之前的最小值的位置(灰度值为70), 对其进行阈值化处理,得到二值图像: cv::Mat thresholded; // 输出二值图像 cv::threshold(image,thresholded,70, // 阈值 255, // 对超过阈值的像素赋值 cv::THRESH_BINARY); // 阈值化类型 得到的二值图像清晰显示出背景/前景的分割情况。

为了适应各种场景,cv::calcHist 函数带有很多参数:

void calcHist(const Mat*images, // 源图像 int nimages, // 源图像的个数(通常为1) const int*channels, // 列出通道 InputArray mask, // 输入掩码(需处理的像素) OutputArray hist, // 输出直方图 int dims, // 直方图的维度(通道数量) const int*histSize, // 每个维度位数 const float**ranges, // 每个维度的范围 bool uniform=true, // true 表示箱子间距相同 bool accumulate=false) // 是否在多次调用时进行累加

大多数情况下,直方图是单个的单通道或三通道图像,但也可以在这个函数中指定一个分布 在多幅图像(即多个cv::Mat)上的多通道图像。这也是把输入图像数组作为函数第一个参数 的原因。第六个参数dims 指明了直方图的维数,例如1 表示一维直方图。在分析多通道图像时, 可以只把它的部分通道用于计算直方图,将需要处理的通道放在维数确定的数组channel 中。 在这个类的实现中只有一个通道,默认为0。直方图用每个维度上的箱子数量(即整数数组 histSize)以及每个维度(由ranges 数组提供,数组中每个元素又是一个二元素数组)上的 最小值(含)和最大值(不含)来描述。你也可以定义一个不均匀的直方图(倒数第二个参数应 设为false),这时需要指定每个箱子的限值。 和很多OpenCV 函数一样,可以使用掩码表示计算时用到的像素(所有掩码值为0 的像素都 不使用)。此外还可以指定两个布尔值类型的附加参数,第一个表示是否采用均匀的直方图(默 认为true),第二个表示是否允许累加多个直方图计算的结果。如果第二个参数为true,那么 图像中的像素数量会累加到输入直方图的当前值中。在计算一组图像的直方图时,就可以使用这 个参数。 得到的直方图存储在cv::Mat 的实例中。事实上,cv::Mat 类可用于操作通用的N 维矩阵。cv::Mat 类定义了适用于一维、二维和三维矩阵的at 方法。正因如此,我们才可 以在getHistogramImage 方法中用下面的代码访问一维直方图的每个箱子: float binVal = hist.at(h); 注意,直方图中的值存储为float 值。

彩色图直方图处理: 计算彩色图像的直方图 我们可以用同一个cv::calcHist 函数计算多通道图像的直方图。例如,若想计算彩色BGR 图像的直方图,可以这样定义一个类:

class ColorHistogram { private: int histSize[3]; // 每个维度的大小 float hranges[2]; // 值的范围(三个维度用同一个值) const float* ranges[3]; // 每个维度的范围 int channels[3]; // 需要处理的通道 public: ColorHistogram() { // 准备用于彩色图像的默认参数 // 每个维度的大小和范围是相等的 histSize[0]= histSize[1]= histSize[2]= 256; hranges[0]= 0.0; // BGR 范围为0~256 hranges[1]= 256.0; ranges[0]= hranges; // 这个类中 ranges[1]= hranges; // 所有通道的范围都相等 ranges[2]= hranges; channels[0]= 0; // 三个通道:B channels[1]= 1; // G channels[2]= 2; // R }

这里的直方图将会是三维的,因此需要为每个维度指定一个范围。本例中的BGR 图像的三 个通道范围都是[0,255]。准备好参数后,就可以用下面的方法计算颜色直方图了: // 计算直方图

cv::Mat getHistogram(const cv::Mat &image) { cv::Mat hist; // 计算直方图 cv::calcHist(&image, 1, // 单幅图像的直方图 channels, // 用到的通道 cv::Mat(), // 不使用掩码 hist, // 得到的直方图 3, // 这是一个三维直方图 histSize, // 箱子数量 ranges // 像素值的范围 ); return hist; }

上述方法返回一个三维的cv::Mat 实例。如果选用含有256 个箱子的直方图,这个矩阵就 有(256)^3 个元素,表示超过1600 万个项目。在很多应用程序中,最好在计算直方图时减少箱子 的数量。也可以使用数据结构cv::SparseMat 表示大型稀疏矩阵(即非零元素非常稀少的矩 阵),这样不会消耗过多的内存。cv::calcHist 函数具有返回这种矩阵的版本,因此只需要简 单地修改一下前面的方法,即可使用cv::SparseMatrix: // 计算直方图

cv::SparseMat getSparseHistogram(const cv::Mat &image) { cv::SparseMat hist(3, // 维数 histSize, // 每个维度的大小 CV_32F); // 计算直方图 cv::calcHist(&image, 1, // 单幅图像的直方图 channels, // 用到的通道 cv::Mat(), // 不使用掩码 hist, // 得到的直方图 3, // 这是三维直方图 histSize, // 箱子数量 ranges // 像素值的范围 ); return hist; }

这是一个三维直方图,画起来比较困难。我们也可以通过显示独立的R、G 和B 通道的直方 图来说明图像中颜色的分布情况。

用查找表修改图像外观:图像直方图提供了利用现有像素强度值进行场景渲染的方法。通过分析图像中像素值的分布 情况,你可以利用这个信息来修改图像,甚至提高图像质量。本节将解释如何用一个简单的映射 函数(称为查找表)来修改图像的像素值。我们即将看到,查找表通常根据直方分布图生成。 查找表是个一对一(或多对一)的函数,定义了如何把像素值转换成新的值。它是一个一维 数组,对于规则的灰度图像,它包含256 个项目。利用查找表的项目i,可得到对应灰度级的新 强度值,如下所示:

newIntensity= lookup[oldIntensity]; OpenCV 中的cv::LUT 函数在图像上应用查找表生成一个新的图像。查找表通常根据直方 图生成,因此在Histogram1D 类中加入了这个函数: static cv::Mat applyLookUp(const cv::Mat& image, // 输入图像 const cv::Mat& lookup) {// uchar 类型的1×256 数组 // 输出图像 cv::Mat result; // 应用查找表 cv::LUT(image,lookup,result); return result; }

实现原理 在图像上应用查找表后会得到一个新图像,新图像的像素强度值被修改为查找表中规定的 值。下面是一个简单的转换过程: // 创建一个图像反转的查找表

cv::Mat lut(1,256,CV_8U); // 256×1 矩阵 for (int i=0; i<256; i++) { // 0 变成255、1 变成254,以此类推 lut.at<uchar>(i)= 255-i; }

这个转换过程对像素强度进行了简单的反转,即强度0 变成255、1 变成254、最后255 变成 0。对图像应用这种查找表后,会生成原始图像的反向图像。使用上一节的图像,得到的结果如 下所示。

伸展直方图以提高图像对比度: 定义一个修改原始图像直方图的查找表可以提高图像的对比度。例如,观察某些图像直 方图可以发现,图中根本没有大于200 的像素值。我们可以通过伸展直方图来生成一个对比度更 高的图像。为此要使用一个百分比阈值,表示伸展后图像的最小强度值(0)和最大强度值(255) 像素的百分比。 我们必须在强度值中找到最小值(imin)和最大值(imax),使得所要求的最小的像素数量 高于阈值指定的百分比。这可以用以下几个循环(其中hist 是计算得到的一维直方图)实现:

// 像素的百分比

float number= image.total()*percentile; // 找到直方图的左极限 int imin = 0; for (float count=0.0; imin < 256; imin++) { // 小于或等于imin 的像素数量必须>number if ((count+=hist.at<float>(imin)) >= number) break; } // 找到直方图的右极限 int imax = 255; for (float count=0.0; imax >= 0; imax--) { // 大于或等于imax 的像素数量必须> number if ((count += hist.at<float>(imax)) >= number) break; }

然后重新映射强度值,使imin 的值变成强度值0,imax 的值变成强度值255。两者之间的 i 进行线性映射: 255.0*(i-imin)/(imax-imin); 伸展1%后的图像如下所示。

伸展过的直方图如下所示。

在彩色图像上应用查找表 通过减色函数,通过修改图像中的BGR 值减少可能的颜色数量。当时的实现 方法是循环遍历图像中的像素,并对每个像素应用减色函数。实际上,更高效的做法是预先计算 好所有的减色值,然后用查找表修改每个像素。利用本节的方法,这很容易实现。下面是新的减 色函数:

void colorReduce(cv::Mat &image, int div=64) { // 创建一维查找表 cv::Mat lookup(1,256,CV_8U); // 定义减色查找表的值 for (int i=0; i<256; i++) lookup.at<uchar>(i)= i/div*div + div/2; // 对每个通道应用查找表 cv::LUT(image,lookup,image); }

这种减色方案之所以能起作用,是因为在多通道图像上应用一维查找表时,同一个查找表会 独立地应用在所有通道上。如果查找表超过一个维度,那么它和所用图像的通道数必须相同。

直方图均衡化: 即通过伸展直方图,使它布满可用强度值的全部范 围。这方法确实可以简单有效地提高图像质量,但很多时候,图像的视觉缺陷并不因为它使用的 强度值范围太窄,而是因为部分强度值的使用频率远高于其他强度值。 某些现象,中等灰度的强度值非常多,而较暗和较亮的像素值则非常稀少。 因此,均衡对所有像素强度值的使用频率可以作为提高图像质量的一种手段。这正是直方图均衡 化这一概念背后的思想,也就是让图像的直方图尽可能地平稳。 OpenCV 提供了一个易用的函数,用于直方图均衡化处理。这个函数的调用方式为: cv::equalizeHist(image,result); 对图像应用该函数后,得到的结果如下所示。

均衡化后图像的直方图如下所示。

直方图均衡化实现原理: 在一个完全均衡的直方图中,所有箱子所包含的像素数量是相等的。这意味着50%像素的强 度值小于128(强度中值),25%像素的强度值小于64,以此类推。这个现象可以用一条规则来 表示:p%像素的强度值必须小于或等于255p%。这条规则用于直方图均衡化处理,表示强度值 i 的映像对应强度值小于i 的像素所占的百分比。因此可以用下面的语句构建所需的查找表: lookup.at(i)= static_cast(255.0p[i]/image.total()); 这里的p[i]是强度值小于或等于i 的像素数量,通常称为累计直方图。这种直方图包含小 于或等于指定强度值的像素数量,而非仅仅包含等于指定强度值的像素数量。前面说过 image.total()返回图像的像素总数,因此p[i]/image.total()就是像素数量的百分比。 一般来说,直方图均衡化会大大改进图像外观,但是改进的效果会因图像可视内容的不同 而不同。

反向投影直方图检测特定图像内容 直方图是图像内容的一个重要特性。如果图像的某个区域含有特定的纹理或物体,这个区域 的直方图就可以看作一个函数,该函数返回某个像素属于这个特殊纹理或物体的概率。

直方图反向投影实现原理: 假设你希望在某幅图像中检测出特定的内容(例如检测出下图中天上的云彩),首先要做的 就是选择一个包含所需样本的感兴趣区域。下图中的该区域就在矩形内部。

在程序中用下面的方法可以得到这个感兴趣区域:

cv::Mat imageROI; imageROI= image(cv::Rect(216,33,24,30)); // 云彩区域 接着提取该ROI 的直方图。使用4.2 节的Histogram1D 类,能轻松获得该直方图: Histogram1D h; cv::Mat hist= h.getHistogram(imageROI);

通过归一化直方图,我们可得到一个函数,由此可得到特定强度值的像素属于这个区域的 概率: cv::normalize(histogram,histogram,1.0); 反向投影直方图的过程包括:从归一化后的直方图中读取概率值并把输入图像中的每个像素 替换成与之对应的概率值。OpenCV 中有一个函数可完成此任务:

cv::calcBackProject(&image, 1, // 一幅图像 channels, // 用到的通道,取决于直方图的维度 histogram, // 需要反向投影的直方图 result, // 反向投影得到的结果 ranges, // 值的范围 255.0 // 选用的换算系数 // 把概率值从1 映射到255 );

得到的结果就是下面的概率分布图。为提高可读性,对图像做了反色处理,属于该区域的概 率从亮(低概率)到暗(高概率),如下所示。

如果对此图做阈值化处理,就能得到最有可能是“云彩”的像素: cv::threshold(result, result, threshold, 255, cv::THRESH_BINARY); 得到的结果如下所示。

实现原理: 前面的结果并不令人满意。因为除了云彩,其他区域也被错误地检测到了。这个概率函数是 从一个简单的灰度直方图提取的,理解这点很重要。很多其他像素的强度值与云彩像素的强度值 是相同的,在对直方图进行反向投影时会用相同的概率值替换具有相同强度值的像素。有一种方 案可提高检测效果,那就是使用色彩信息。要实现这点,需改变对cv::calBackProject 的调 用方式 cv::calBackProject 函数和cv::calcHist 有些类似。一个像素的值对应直方图的一个 箱子(可能是多维的)。但cv::calBackProject 不会增加箱子的计数,而是把从箱子读取的值 赋给反向投影图像中对应的像素。函数的第一个参数指明输入的图像(通常只有一个),接着需 要指明使用的通道数量。这里传递给函数的直方图是一个输入参数,它的维度数要与通道列表数 组的维度数一致。与cv::calcHist 函数一样,这里的ranges 参数用数组形式指定了输入直 方图的箱子边界。该数组以浮点数组为元素,每个数组元素表示一个通道的取值范围(最小值 和最大值)。 输出结果是一幅图像,包含计算得到的概率分布图。由于每个像素已经被替换成直方图中对 应箱子处的值,因此输出图像的值范围是0.0~1.0(假定输入的是归一化直方图)。最后一个参 数是换算系数,可用来重新缩放这些值。

用均值平移算法查找目标: 直方图反向投影的结果是一个概率分布图,表示一个指定图像片段出现在特定位置的概率。 如果我们已经知道图像中某个物体的大致位置,就可以用概率分布图找到物体的准确位置。窗口 中概率最大的位置就是物体最可能出现的位置。因此,我们可以从一个初始位置开始,在周围反 复移动以提高局部匹配概率,也许就能找到物体的准确位置。这个实现方法称为均值平移算法。

这次采用HSV 色彩空间的色调通道来描述物体。这意味着需要把图像转换成HSV 色彩空间 并提取色调通道,然后计算指定ROI 的一维色调直方图。参见以下代码:

// 读取参考图像 cv::Mat image= cv::imread("baboon01.jpg"); // 狒狒脸部的ROI cv::Rect rect(110, 45, 35, 45); cv::Mat imageROI= image(rect); // 得到狒狒脸部的直方图 int minSat=65; ColorHistogram hc; cv::Mat colorhist= hc.getHueHistogram(imageROI,minSat); 我们在ColorHistogram 类中增加了一个简便的方法来获得色调直方图,代码如下所示: // 计算一维色调直方图 // BGR 的原图转换成HSV // 忽略低饱和度的像素 cv::Mat getHueHistogram(const cv::Mat &image, int minSaturation=0) { cv::Mat hist; // 转换成HSV 色彩空间 cv::Mat hsv; cv::cvtColor(image, hsv, CV_BGR2HSV); // 掩码(可能用到,也可能用不到) cv::Mat mask; // 根据需要创建掩码 if (minSaturation>0) { // 将3 个通道分割进3 幅图像 std::vector<cv::Mat> v; cv::split(hsv,v); // 屏蔽低饱和度的像素 cv::threshold(v[1],mask,minSaturation, 255, cv::THRESH_BINARY); } // 准备一维色调直方图的参数 hranges[0]= 0.0; // 范围为0~180 hranges[1]= 180.0; channels[0]= 0; // 色调通道 // 计算直方图 cv::calcHist(&hsv, 1, // 只有一幅图像的直方图 channels, // 用到的通道 mask, // 二值掩码 hist, // 生成的直方图 1, // 这是一维直方图 histSize, // 箱子数量 ranges // 像素值范围 ); return hist; } 然后把得到的直方图传给ContentFinder 类的实例,代码如下所示: ContentFinder finder; finder.setHistogram(colorhist);

现在打开第二幅图像,我们想在它上面定位狒狒的脸部。首先,需要把这幅图像转换成HSV 色彩空间,然后对第一幅图像的直方图做反向投影,参见下面的代码:

image= cv::imread("baboon3.jpg"); // 转换成HSV 色彩空间 cv::cvtColor(image, hsv,CV_BGR2HSV); // 得到色调直方图的反向投影 int ch[1]={0}; finder.setThreshold(-1.0f); // 不做阈值化 cv::Mat result= finder.find(hsv,0.0f,180.0f,ch); rect 对象是一个初始矩形区域(即初始图像中狒狒脸部的位置),现在OpenCV 的cv:: meanShift 算法将会把它修改成狒狒脸部的新位置,代码如下所示: // 窗口初始位置 cv::Rect rect(110,260,35,40); // 用均值偏移法搜索物体 cv::TermCriteria criteria( cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS, 10, // 最多迭代10 次 1); // 或者重心移动距离小于1 个像素 cv::meanShift(result,rect,criteria);
最新回复(0)