OpenCV计算机视觉编程篇四《用直方图统计像素》

OpenCV 虚幻 748℃ 0评论

前言

前期回顾: OpenCV计算机视觉编程篇三《处理图像的颜色》
上面这篇里面写了操作像素相关。

本章包括以下内容:

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

4.1 简介

图像是由不同数值(颜色)的像素构成的,像素值在整幅图像中的分布情况是该图像的一个重要属性。本章将介绍图像直方图的概念,你将学会如何计算直方图、如何用直方图修改图像的 外观,还可以用直方图来标识图像的内容,检测图像中特定的物体或纹理。本章将讲解其中的部 分技术。

4.2 计算图像直方图

图像由各种数值的像素构成。例如在单通道灰度图像中,每个像素都有一个 0(黑色)~255 (白色)的整数。对于每个灰度,都有不同数量的像素分布在图像内,具体取决于图片内容。

直方图是一个简单的表格,表示一幅图像(有时是一组图像)中具有某个值的像素的数量。 因此,灰度图像的直方图有 256 个项目,也叫箱子(bin)。0 号箱子提供值为 0 的像素的数量, 1 号箱子提供值为 1 的像素的数量,以此类推。很明显,如果把直方图的所有箱子进行累加,得 到的结果就是像素的总数。你也可以把直方图归一化,即所有箱子的累加和等于 1。这时,每个 箱子的数值表示对应的像素数量占总数的百分比。

4.2.1 准备工作

本章的前 4 节会用到这幅图像。

20201227193334481 - OpenCV计算机视觉编程篇四《用直方图统计像素》

4.2.2 如何实现

要在 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; 

使用本章开始时的图像,部分显示的值如下所示:

Value 7 = 159
Value 8 = 208
Value 9 = 271
Value 10 = 288
Value 11 = 340
Value 12 = 418
Value 13 = 432
Value 14 = 472
Value 15 = 525 

显然,只看这一系列数值很难得到任何有意义的信息。因此比较实用的做法是以函数的方式 显示直方图,例如用柱状图。用下面这几种方法可创建这种图形:

// 计算一维直方图,并返回它的图像
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)); 

得到的结果如下图所示。

20201227193401424 - OpenCV计算机视觉编程篇四《用直方图统计像素》

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

cv::Mat thresholded; // 输出二值图像
cv::threshold(image,thresholded,70, // 阈值
 255, // 对超过阈值的像素赋值
 cv::THRESH_BINARY); // 阈值化类型

得到的二值图像清晰显示出背景/前景的分割情况。

20201227193410767 - OpenCV计算机视觉编程篇四《用直方图统计像素》

4.2.3 实现原理

为了适应各种场景,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 维矩阵。 第 2 章讲过,cv::Mat 类定义了适用于一维、二维和三维矩阵的 at 方法。正因如此,我们才可 以在 getHistogramImage 方法中用下面的代码访问一维直方图的每个箱子:

float binVal = hist.at<float>(h); 

注意,直方图中的值存储为 float 值。

4.2.4 扩展阅读

本节中的 Histogram1D 类简化了 cv::calcHist 函数,把它限定为只用于一维直方图。 这对灰度图像是有用的,但是怎么处理彩色图像呢?

计算彩色图像的直方图

我们可以用同一个 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 通道的直方 图来说明图像中颜色的分布情况。

4.2.5 参阅

  • 4.5 节将使用颜色直方图来检测特定的图像内容。

4.3 利用查找表修改图像外观

图像直方图提供了利用现有像素强度值进行场景渲染的方法。通过分析图像中像素值的分布 情况,你可以利用这个信息来修改图像,甚至提高图像质量。本节将解释如何用一个简单的映射 函数(称为查找表)来修改图像的像素值。我们即将看到,查找表通常根据直方分布图生成。

4.3.1 如何实现

查找表是个一对一(或多对一)的函数,定义了如何把像素值转换成新的值。它是一个一维 数组,对于规则的灰度图像,它包含 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;
} 

4.3.2 实现原理

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

// 创建一个图像反转的查找表
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。对图像应用这种查找表后,会生成原始图像的反向图像。使用上一节的图像,得到的结果如 下所示。

202012271935073 - OpenCV计算机视觉编程篇四《用直方图统计像素》

4.3.3 扩展阅读

对于需要更换全部像素强度值的程序,都可以使用查找表。但是这个转换过程必须是针对整 幅图像的。也就是说,一个强度值对应的全部像素都必须使用同一种转换方法。

  1. 伸展直方图以提高图像对比度

定义一个修改原始图像直方图的查找表可以提高图像的对比度。例如,观察 4.2 节的图像直 方图可以发现,图中根本没有大于 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%后的图像如下所示。

20201227193522587 - OpenCV计算机视觉编程篇四《用直方图统计像素》

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

20201227193531966 - OpenCV计算机视觉编程篇四《用直方图统计像素》

  1. 在彩色图像上应用查找表

第 2 章定义了一个减色函数,通过修改图像中的 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);
} 

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

4.3.4 参阅

  • 4.4 节将展示另一种增强图像对比度的方法。

4.4 直方图均衡化

上节介绍了一种增强图像对比度的方法,即通过伸展直方图,使它布满可用强度值的全部范 围。这方法确实可以简单有效地提高图像质量,但很多时候,图像的视觉缺陷并不因为它使用的 强度值范围太窄,而是因为部分强度值的使用频率远高于其他强度值。4.2 节显示的直方图就是 此类现象的一个很好的例子——中等灰度的强度值非常多,而较暗和较亮的像素值则非常稀少。 因此,均衡对所有像素强度值的使用频率可以作为提高图像质量的一种手段。这正是直方图均衡 化这一概念背后的思想,也就是让图像的直方图尽可能地平稳。

4.4.1 如何实现

OpenCV 提供了一个易用的函数,用于直方图均衡化处理。这个函数的调用方式为:

cv::equalizeHist(image,result); 

对图像应用该函数后,得到的结果如下所示。

20201227193544288 - OpenCV计算机视觉编程篇四《用直方图统计像素》

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

20201227193556452 - OpenCV计算机视觉编程篇四《用直方图统计像素》

当然,因为查找表是针对整幅图像的多对一的转换过程,所以直方图是不能做到完全平稳的。 但是可以看出,直方图的整体分布情况已经比原来均衡多了。

4.4.2 实现原理

在一个完全均衡的直方图中,所有箱子所包含的像素数量是相等的。这意味着 50%像素的强 度值小于 128(强度中值),25%像素的强度值小于 64,以此类推。这个现象可以用一条规则来 表示:p%像素的强度值必须小于或等于 255*p%。这条规则用于直方图均衡化处理,表示强度值 i 的映像对应强度值小于 i 的像素所占的百分比。因此可以用下面的语句构建所需的查找表:

lookup.at<uchar>(i)= static_cast<uchar>(255.0*p[i]/image.total()); 

这里的 p[i]是强度值小于或等于 i 的像素数量,通常称为累计直方图。这种直方图包含小 于或等于指定强度值的像素数量,而非仅仅包含等于指定强度值的像素数量。前面说过 image.total()返回图像的像素总数,因此 p[i]/image.total()就是像素数量的百分比。

一般来说,直方图均衡化会大大改进图像外观,但是改进的效果会因图像可视内容的不同 而不同。

4.5 反向投影直方图检测特定图像内容

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

4.5.1 如何实现

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

20201227193609324 - OpenCV计算机视觉编程篇四《用直方图统计像素》

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

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
); 

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

20201227193620894 - OpenCV计算机视觉编程篇四《用直方图统计像素》

如果对此图做阈值化处理,就能得到最有可能是“云彩”的像素:

cv::threshold(result, result, threshold, 255, cv::THRESH_BINARY); 

得到的结果如下所示。

2020122719363068 - OpenCV计算机视觉编程篇四《用直方图统计像素》

4.5.2 实现原理

前面的结果并不令人满意。因为除了云彩,其他区域也被错误地检测到了。这个概率函数是 从一个简单的灰度直方图提取的,理解这点很重要。很多其他像素的强度值与云彩像素的强度值 是相同的,在对直方图进行反向投影时会用相同的概率值替换具有相同强度值的像素。有一种方 案可提高检测效果,那就是使用色彩信息。要实现这点,需改变对 cv::calBackProject 的调 用方式,4.5.3 节将详细介绍这个函数。

cv::calBackProject 函数和 cv::calcHist 有些类似。一个像素的值对应直方图的一个 箱子(可能是多维的)。但 cv::calBackProject 不会增加箱子的计数,而是把从箱子读取的值 赋给反向投影图像中对应的像素。函数的第一个参数指明输入的图像(通常只有一个),接着需 要指明使用的通道数量。这里传递给函数的直方图是一个输入参数,它的维度数要与通道列表数 组的维度数一致。与 cv::calcHist 函数一样,这里的 ranges 参数用数组形式指定了输入直 方图的箱子边界。该数组以浮点数组为元素,每个数组元素表示一个通道的取值范围(最小值 和最大值)。

输出结果是一幅图像,包含计算得到的概率分布图。由于每个像素已经被替换成直方图中对 应箱子处的值,因此输出图像的值范围是 0.0~1.0(假定输入的是归一化直方图)。最后一个参 数是换算系数,可用来重新缩放这些值。

4.5.3 扩展阅读

现在来学习如何在直方图反向映射算法中使用色彩信息。

反向映射颜色直方图

多维度直方图也可以在图像上进行反向映射。我们定义一个封装反向映射过程的类,首先定 义必需的参数并初始化:

class ContentFinder {
 private:
 // 直方图参数
 float hranges[2];
 const float* ranges[3];
 int channels[3];
 float threshold; // 判断阈值
 cv::Mat histogram; // 输入直方图
public:
ContentFinder() : threshold(0.1f) {
 // 本类中所有通道的范围相同
 ranges[0]= hranges;
 ranges[1]= hranges;
 ranges[2]= hranges;
} 

这里引入了一个阈值参数,用于创建显示检测结果的二值分布图。如果这个参数设为负数, 就会返回原始的概率分布图。输入的直方图用下面的方法归一化(但这不是必须的):

// 设置引用的直方图
void setHistogram(const cv::Mat& h) {
 histogram= h;
 cv::normalize(histogram,histogram,1.0);
} 

要反向投影直方图,只需指定图像、范围(这里假定所有通道的范围是相同的)和所用通道 的列表。方法 find 可以进行反向投影。它有两个版本,一个使用图像的三个通道,并调用通用 版本的方法:

// 使用全部通道,范围[0,256]
cv::Mat find(const cv::Mat& image) {
 cv::Mat result;
 hranges[0]= 0.0; // 默认范围[0,256]hranges[1]= 256.0;
 channels[0]= 0; // 三个通道
 channels[1]= 1;
 channels[2]= 2;
 return find(image, hranges[0], hranges[1], channels);
}
// 查找属于直方图的像素
cv::Mat find(const cv::Mat& image, float minValue, float maxValue,
 int *channels) {
 cv::Mat result;
 hranges[0]= minValue;
 hranges[1]= maxValue;
 // 直方图的维度数与通道列表一致
 for (int i=0; i<histogram.dims; i++)
 this->channels[i]= channels[i];
 cv::calcBackProject(&image, 1, // 只使用一幅图像
 channels, // 通道
 histogram, // 直方图
 result, // 反向投影的图像
 ranges, // 每个维度的值范围
 255.0 // 选用的换算系数
 // 把概率值从 1 映射到 255
 );
}
// 对反向投影结果做阈值化,得到二值图像
if (threshold>0.0)
 cv::threshold(result, result,255.0*threshold,
 255.0, cv::THRESH_BINARY);
 return result;
} 

现在把前面用过的图像换成彩色版本(访问本书的网站查看彩色图像),并使用一个 BGR 直 方图。这次来检测天空区域。首先装载彩色图像,定义 ROI,然后计算经过缩减的色彩空间上的 3D 直方图,代码如下所示:

// 装载彩色图像
ColorHistogram hc;
cv::Mat color= cv::imread("waves.jpg");
// 提取 ROI
imageROI= color(cv::Rect(0,0,100,45)); // 蓝色天空区域
// 取得 3D 颜色直方图(每个通道含 8 个箱子)
hc.setSize(8); // 8×8×8
cv::Mat shist= hc.getHistogram(imageROI);
下一步是计算直方图,并用 find 方法检测图像中的天空区域:
// 创建内容搜寻器
ContentFinder finder;
// 设置用来反向投影的直方图
finder.setHistogram(shist);
finder.setThreshold(0.05f);
// 取得颜色直方图的反向投影
Cv::Mat result= finder.find(color);

上一节的彩色图像的检测结果如下所示。

20201227193648838 - OpenCV计算机视觉编程篇四《用直方图统计像素》

通常来说,采用 BGR 色彩空间识别图像中的彩色物体并不是最好的方法。为了提高可靠性, 我们在计算直方图之前减少了颜色的数量(要知道原始 BGR 色彩空间有超过 1600 万种颜色)。 提取的直方图代表了天空区域的典型颜色分布情况。用它在其他图像上反向投影,也能检测到天 空区域。注意,用多个天空图像构建直方图可以提高检测的准确性。

本例中,计算稀疏直方图可以减少内存使用量。你可以使用 cv::SparseMat 重做该实验。 另外,如果要寻找色彩鲜艳的物体,使用 HSV 色彩空间的色调通道可能会更加有效。在其他情 况下,最好使用感知上均匀的色彩空间(例如 Lab*)的色度组件。

4.5.4 参阅

  • 4.6节将用HSV色彩空间检测图像中的物体。检测图像内容的方法很多,这是其中的一种。
  • 第 3 章的最后两节介绍了多种色彩空间,可用于直方图反向投影。

4.6 用均值平移算法查找目标

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

4.6.1 如何实现

假设我们已经识别出一个感兴趣的物体(例如狒狒的脸),如下图所示:

20201227193704507 - OpenCV计算机视觉编程篇四《用直方图统计像素》

这次采用 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); 

脸部的初始位置(红色框)和新位置(绿色框)显示如下。

20201227193718430 - OpenCV计算机视觉编程篇四《用直方图统计像素》

4.6.2 实现原理

本例为了突出被寻找物体的特征,使用了 HSV 色彩空间的色调分量。之所以这样做,是 因为狒狒脸部有非常独特的粉红色,使用像素的色调很容易标识狒狒脸部,因此第一步就是把 图像转换成 HSV 色彩空间。使用 CV_BGR2HSV 标志转换图像后,得到的第一个通道就是色调 分量。这是一个 8 位分量,值范围为 0~180(如果使用 cv::cvtColor,转换后的图像与原始 图像的类型就会是相同的)。为了提取色调图像,cv::split 函数把三通道的 HSV 图像分割 成三个单通道图像。这三幅图像存放在一个 std::vector 实例中,并且色调图像是向量的第 一个入口(即索引为 0)。

在使用颜色的色调分量时,要把它的饱和度考虑在内(饱和度是向量的第二个入口),这一 点通常很重要。如果颜色的饱和度很低,它的色调信息就会变得不稳定且不可靠。这是因为低饱 和度颜色的 B、G 和 R 分量几乎是相等的,这导致很难确定它所表示的准确颜色。因此,我们决定忽略低饱和度颜色的色彩分量,也就是不把它们统计进直方图中(在 getHueHistogram 方法 中使用 minSat 参数可屏蔽掉饱和度低于此阈值的像素)。

均值偏移算法是一个迭代过程,用于定位概率函数的局部最大值,方法是寻找预定义窗口内 部数据点的重心或加权平均值。然后,把窗口移动到重心的位置,并重复该过程,直到窗口中心 收敛到一个稳定的点。OpenCV 实现该算法时定义了两个停止条件:迭代次数达到最大值 (MAX_ITER);窗口中心的偏移值小于某个限值(EPS),可认为该位置收敛到一个稳定点。这两 个条件存储在一个 cv::TermCriteria 实例中。cv::meanShift 函数返回已经执行的迭代次 数。显然,结果的好坏取决于指定初始位置提供的概率分布图的质量。注意,这里用颜色直方图 表示图像的外观。也可以用其他特征的直方图(例如边界方向直方图)来表示物体。

4.6.3 参阅

  • 均值偏移算法广泛应用于视觉追踪,第 13 章将会详细探讨目标跟踪的问题。
  • D. Comaniciu 和 P. Meer 发表于 2002 年发表在 IEEE Transactions on Pattern Analysis and Machine Intelligence第 5期第 24卷上的文章“Mean Shift: A robust approach toward feature space analysis”首次提出了均值偏移算法。
  • OpenCV 也提供了 CamShift 算法的具体实现方法。这个算法是均值偏移算法的改进版本, 允许修改窗口的尺寸和方向。

4.7 比较直方图搜索相似图像

基于内容的图像检索是计算机视觉的一个重要课题。它包括根据一个已有的基准图像,找出 一批内容相似的图像。我们已经学过,直方图是标识图像内容的一种有效方式,因此值得研究一 下能否用它来解决基于内容的图像检索问题。

这里的关键是,要仅靠比较它们的直方图就测量出两幅图像的相似度。我们需要定义一个测 量函数,来评估两个直方图之间的差异程度或相似程度。人们已经提出了很多测量方法,OpenCV 在 cv::compareHist 函数的实现过程中使用了其中的一些方法。

4.7.1 如何实现

为了将一个基准图像与一批图像进行对比并找出其中与它最相似的图像,我们创建了类 ImageComparator。这个类引用了一个基准图像和一个输入图像(连同它们的直方图)。另外, 因为要用颜色直方图来进行比较,因此 ImageComparator 中用到了 ColorHistogram 类:

class ImageComparator {
 private:
 cv::Mat refH; // 基准直方图
 cv::Mat inputH; // 输入图像的直方图
 ColorHistogram hist; // 生成直方图
 int nBins; // 每个颜色通道使用的箱子数量
 public:
 ImageComparator() :nBins(8) {
 } 

为了得到更加可靠的相似度测量结果,需要在计算直方图时减少箱子的数量。可以在类中指 定每个 BGR 通道所用的箱子数量。

用一个适当的设置函数指定基准图像,同时计算参考直方图,代码如下所示:

// 设置并计算基准图像的直方图
void setReferenceImage(const cv::Mat& image) {
 hist.setSize(nBins);
 refH= hist.getHistogram(image);
} 

最后,compare 方法会将基准图像和指定的输入图像进行对比。下面的方法返回一个分数, 表示两幅图像的相似程度:

// 用 BGR 直方图比较图像
double compare(const cv::Mat& image) {
 inputH= hist.getHistogram(image);
 // 用交叉法比较直方图
 return cv::compareHist(refH,inputH, cv::HISTCMP_INTERSECT);
} 

前面的类可用来检索与给定的基准图像类似的图像。类的实例中使用了基准图像,代码如下 所示:

ImageComparator c;
c.setReferenceImage(image); 

这里用 4.5 节中海滩图的彩色版本作为基准图像,并将这幅图像与后面的一系列图像进行对 比,其中相似度高的放前面,相似度低的放后面,如下所示。

20201227193735646 - OpenCV计算机视觉编程篇四《用直方图统计像素》

4.7.2 实现原理

大多数直方图比较方法都是基于逐个箱子进行比较的。正因如此,在测量两个颜色直方图的 相似度时,把邻近颜色组合进同一个箱子显得十分重要。对 cv::compareHist 的调用非常简单, 只需要输入两个直方图,函数就会返回它们的差距。你可以通过一个标志参数指定想要使用的测 量方法。ImageComparator 类使用了交叉点方法(带有 cv::HISTCMP_INTERSECT 标志)。该 方法只是逐个箱子地比较每个直方图中的数值,并保存最小的值。然后把这些最小值累加,作为 相似度测量值。因此,两个没有相同颜色的直方图得到的交叉值为 0,而两个完全相同的直方图 得到的值就等于像素总数。

其他可用的算法有:卡方测量法(cv::HISTCMP_CHISQR 标志)累加各箱子的归一化平方差; 关联性算法(cv::HISTCMP_CORREL 标志)基于信号处理中的归一化交叉关联操作符测量两个 信号的相似度;Bhattacharyya 测量法(cv::HISTCMP_BHATTACHARYYA 标志)和 Kullback-Leibler 发散度(cv::HISTCMP_KL_DIV 标志)都用在统计学中,评估两个概率分布的相似度。

4.7.3 参阅

  • OpenCV 文档详细描述了不同的直方图比较方法中使用的公式。
  • 推土机距离(Earth Mover Distance)是另一种流行的直方图比较方法,在 OpenCV 中通过 cv::EMD 函数实现。这种方法的主要优势在于,它在评估两个直方图的相似度时,考虑 了在邻近箱子中发现的数值。具体描述可查看Y. Rubner、C. Tomasi和L. J. Guibas于2000 年发表在 Int. Journal of Computer Vision 第 2 期第 40 卷第 99 页至第 121 页的“The Earth Mover’s Distance as a Metric for Image Retrieval”。

4.8 用积分图像统计像素

前面几节讲了直方图的计算方法,即遍历图像的全部像素并累计每个强度值在图像中出现的 次数。我们也看到,有时只需要计算图像中某个特定区域的直方图。实际上,累计图像某个子区 域内的像素总数是很多计算机视觉算法中的常见过程。现在假设需要对图像中的多个感兴趣区域 计算几个此类直方图,这些计算过程马上都会变得非常耗时。这种情况下,有一个工具可以极大 地提高统计图像子区域像素的效率,那就是积分图像。

使用积分图像统计图像感兴趣区域的像素是一种高效的方法。它在程序中的应用非常广泛, 例如用于计算基于不同大小的滑动窗口。

本节将讲解积分图像背后的原理。这里的目标是说明如何只用三次算术运算,就能累加一个 矩形区域的像素。在学会这个概念后,4.8.3 节将展示两个有效使用积分图像的实例。

4.8.1 如何实现

本节将使用下面的图像来做演示,识别出图像中的一个感兴趣区域,区域内容为一个骑自行 车的女孩。

20201227193755657 - OpenCV计算机视觉编程篇四《用直方图统计像素》

在累加多个图像区域的像素时,积分图像显得非常有用。通常来说,要获得感兴趣区域全部 像素的累加和,常规的代码如下所示:

// 打开图像
cv::Mat image= cv::imread("bike55.bmp",0);
// 定义图像的 ROI(这里为骑自行车的女孩)
int xo=97, yo=112;
int width=25, height=30;
cv::Mat roi(image,cv::Rect(xo,yo,width,height));
// 计算累加值
// 返回一个多通道图像下的 Scalar 数值
cv::Scalar sum= cv::sum(roi); 

cv::sum 函数只是遍历区域内的所有像素,并计算累加和。使用积分图像后,只需要三次 加法运算即可实现该功能。不过你得先计算积分图像,代码如下所示:

// 计算积分图像
cv::Mat integralImage;
cv::integral(image,integralImage,CV_32S); 

可以在积分图像上用简单的算术表达式获得同样的结果(下一节会详细解释),代码为:

// 用三个加/减运算得到一个区域的累加值
int sumInt= integralImage.at<int>(yo+height,xo+width)–
 integralImage.at<int>(yo+height,xo)–
 integralImage.at<int>(yo,xo+width)+
 integralImage.at<int>(yo,xo);

两种做法得到的结果是一样的。但计算积分图像需要遍历全部像素,因此速度比较慢。关键 在于,一旦这个初始计算完成,你只需要添加四个像素就能得到感兴趣区域的累加和,与区域大 小无关。 因此,如果需要在多个尺寸不同的区域上计算像素累加和,最好采用积分图像。

4.8.2 实现原理

上一节简单演示了积分图像的“神奇”功能,即可用来快速计算矩形区域内的像素累加和, 并通过演示简要介绍了积分图像的概念。为了理解积分图像的实现原理,我们先对它下一个定义: 取图像左上方的全部像素计算累加和,并用这个累加和替换图像中的每一个像素,用这种方式得 到的图像称为积分图像。计算积分图像时,只需对图像扫描一次。实际上,当前像素的积分值等 于上方像素的积分值加上当前行的累计值。因此积分图像就是一个包含像素累加和的新图像。为 防止溢出,积分图像的值通常采用 int 类型(CV_32S)或 float 类型(CV_32F)。例如在下图 中,积分图像的像素 A 包含左上角区域,即双阴影线图案标识的区域的像素的累加和。

20201227193813220 - OpenCV计算机视觉编程篇四《用直方图统计像素》

计算完积分图像后,只需要访问四个像素就可以得到任何矩形区域的像素累加和。这里解释 一下原因。再来看上面的图片,计算由 A、B、C、D 四个像素表示区域的像素累加和,先读取 D 的积分值,然后减去 B 的像素值和 C 的左手边区域的像素值。但是这样就把 A 左上角的像素累 加和减了两次,因此需要重新加上 A 的积分值。所以计算 A、B、C、D 区域内的像素累加的正 式公式为:ABC + D。如果用 cv::Mat 方法访问像素值,公式可转换成以下代码:

// 窗口的位置是(xo,yo),尺寸是 width×height
return (integralImage.at<cv::Vec<T,N>>(yo+height,xo+width)-
 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)); 

不管感兴趣区域的尺寸有多大,使用这种方法计算的复杂度是恒定不变的。注意,为了简化, 这里使用了 cv::Mat 类的 at 方法,它访问像素值的效率并不是最高的(参见第 2 章)。4.8.3 节 将讨论这方面的内容,通过两个例子说明积分图像在效率上的优势。

4.8.3 扩展阅读

积分图像适合用来执行多次像素累计值的统计。本段将通过介绍自适应阈值化的概念,说明 积分图像的使用方法。在需要快速计算多个窗口的直方图时,积分图像非常有用。本节也将对此 进行解释。

  1. 自适应的阈值化

通过对图像应用阈值来创建二值图像是从图像中提取有意义元素的好方法。假设有下面这个 关于本书的图像。

2020122719382685 - OpenCV计算机视觉编程篇四《用直方图统计像素》

为了分析图像中的文字,对该图像应用一个阈值,代码如下所示:

// 使用固定的阈值
cv::Mat binaryFixed;
cv::threshold(image,binaryFixed,70,255,cv::THRESH_BINARY); 

得到如下结果。

20201227193835507 - OpenCV计算机视觉编程篇四《用直方图统计像素》

实际上,不管选用什么阈值,图像都会丢失一部分文本,还有部分文本会消失在阴影下。要 解决这个问题,有一个办法就是采用局部阈值,即根据每个像素的邻域计算阈值。这种策略称为 自适应阈值化,将每个像素的值与邻域的平均值进行比较。如果某像素的值与它的局部平均值差 别很大,就会被当作异常值在阈值化过程中剔除。

因此自适应阈值化需要计算每个像素周围的局部平均值。这需要多次计算图像窗口的累计 值,可以通过积分图像提高计算效率。正因为如此,方法的第一步就是计算积分图像:

// 计算积分图像
cv::Mat iimage;
cv::integral(image,iimage,CV_32S); 

现在就可以遍历全部像素,并计算方形邻域的平均值了。我们也可以使用 IntegralImage类来实现这个功能,但是这个类在访问像素时使用了效率很低的 at 方法。根据第 2 章学过的方法, 我们可以使用指针遍历图像以提高效率,循环代码如下所示:

int blockSize= 21; // 邻域的尺寸
int threshold=10; // 像素将与(mean-threshold)进行比较
// 逐行
int halfSize= blockSize/2;
for (int j=halfSize; j<nl-halfSize-1; j++) {
 // 得到第 j 行的地址
 uchar* data= binary.ptr<uchar>(j);
 int* idata1= iimage.ptr<int>(j-halfSize);
 int* idata2= iimage.ptr<int>(j+halfSize+1);
 // 一个线条的每个像素
 for (int i=halfSize; i<nc-halfSize-1; i++) {
 // 计算累加值
 int sum= (idata2[i+halfSize+1]-data2[i-halfSize]-
 idata1[i+halfSize+1]+idata1[i-halfSize])
 /(blockSize*blockSize);
 // 应用自适应阈值
 if (data[i]<(sum-threshold))
 data[i]= 0;
 else
 data[i]=255;
 }
} 

本例使用了 21×21 的邻域。为计算每个平均值,我们需要访问界定正方形邻域的四个积分像 素:两个在标有 idata1 的线条上,另两个在标有 idata2 的线条上。将当前像素与计算得到的 平均值进行比较。为了确保被剔除的像素与局部平均值有明显的差距,这个平均值要减去阈值(这 里设为 10)。由此得到下面的二值图像。

20201227193848405 - OpenCV计算机视觉编程篇四《用直方图统计像素》

很明显,这比用固定阈值得到的结果好得多。自适应阈值化是一种常用的图像处理技术。 OpenCV 中也实现了这种方法:

cv::adaptiveThreshold(image, // 输入图像
 binaryAdaptive, // 输出二值图像
 255, // 输出的最大值
 cv::ADAPTIVE_THRESH_MEAN_C, // 方法
 cv::THRESH_BINARY, // 阈值类型
 blockSize, // 块的大小
 threshold); // 使用的阈值

调用这个函数得到的结果与使用积分图像的结果完全相同。另外,除了在阈值化中使用局部 平均值,本例中的函数还可以使用高斯(Gaussian)加权累计值(该方法的标志为 ADAPTIVE_ THRESH_GAUSSIAN_C)。有意思的是,这种实现方式要比调用 cv::adaptiveThreshold 稍微 快一些。

最后需要注意,我们也可以用 OpenCV 的图像运算符来编写自适应阈值化过程。具体方法如 下所示:

cv::Mat filtered;
cv::Mat binaryFiltered;
// boxFilter 计算矩形区域内像素的平均值
cv::boxFilter(image,filtered,CV_8U,cv::Size(blockSize,blockSize));
// 检查像素是否大于(mean + threshold)
binaryFiltered= image>= (filtered-threshold); 

图像滤波的内容将在第 6 章介绍。

  1. 用直方图实现视觉追踪

通过前面几节的学习,我们知道可用直方图表示物体外观的全局特征。本节将搜寻一个所呈 现直方图与目标物体相似的图像区域,演示如何在图像中定位物体,以此说明积分图像的用途。 我们在 4.6 节实现了这个功能,用的是直方图反向投影概念和通过均值偏移局部搜索的方法。这 次我们在整幅图像上显式地搜索具有类似直方图的区域,以此找到物体。

由 0 和 1 组成的二值图像生成积分图像是一种特殊情况,这时的积分累计值就是指定区域内 值为 1 的像素总数。本节将利用这一现象计算灰度图像的直方图。

cv::integral 函数也可用于多通道图像。你可以充分利用这点,用积分图像计算图像子 区域的直方图。只需简单地把图像转换成由二值平面组成的多通道图像,每个平面关联直方图 的一个箱子,并显示哪些像素的值会进入该箱子。下面的函数将从一个灰度图像创建这样的多 图层图像:

// 转换成二值图层组成的多通道图像
// nPlanes 必须是 2 的幂
void convertToBinaryPlanes(const cv::Mat& input,
 // 需要屏蔽的位数
 int n= 8-static_cast<int>(
 log(static_cast<double>(nPlanes))/log(2.0));
 // 用来消除最低有效位的掩码
 uchar mask= 0xFF<<n;
 // 创建二值图像的向量
 std::vector<cv::Mat> planes;
 // 消除最低有效位,箱子数减为 nBins
 cv::Mat reduced= input&mask;
 // 计算每个二值图像平面
 for (int i=0; i<nPlanes; i++) {
 // 将每个等于 i<<shift 的像素设为 1
 planes.push_back((reduced==(i<<n))&0x1);
 }
 // 创建多通道图像
 cv::merge(planes,output);
} 

你也可以把积分图像的计算过程封装进模板类中:

template <typename T, int N>
class IntegralImage {
 cv::Mat integralImage;
 public:
 IntegralImage(cv::Mat image) {
 // 计算积分图像(很耗时)
 cv::integral(image,integralImage,
 cv::DataType<T>::type);
 }
 // 通过访问四个像素,计算任何尺寸子区域的累计值
 cv::Vec<T,N> operator()(int xo, int yo, int width, int height) {
 // (xo,yo)处的窗口,尺寸为 width×height
 return (integralImage.at<cv::Vec<T,N>>(yo+height,xo+width)-
 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));
 }
}; 

我们在前面的图像中识别出了骑车的女孩,现在要在后面的图像中找到她。首先计算原始图 像中女孩的直方图,这可通过 4.2 节创建的 Histogram1D 类实现。以下代码将生成 16 个箱子的 直方图:

// 16 个箱子的直方图
Histogram1D h;
h.setNBins(16);
// 计算图像中 ROI 的直方图
cv::Mat refHistogram= h.getHistogram(roi); 

这个直方图将作为基准,在下面的图像中定位目标(即骑车的女孩)。

假设我们仅知道图像中女孩在水平方向移动。因为需要对不同的位置计算很多直方图,我们 先做准备工作,即计算积分图像。参见以下代码:

// 首先创建 16 个平面的二值图像
cv::Mat planes;
convertToBinaryPlanes(secondIimage,planes,16);
// 然后计算积分图像
IntegralImage<float,16> intHistogram(planes); 

执行搜索时,循环遍历可能出现目标的位置,并将它的直方图与基准直方图做比较,目的是 找到与直方图最相似的位置,参见以下代码:

double maxSimilarity=0.0;
int xbest, ybest;
// 遍历原始图像中女孩位置周围的水平长条
for (int y=110; y<120; y++) {
 for (int x=0; x<secondImage.cols-width; x++) {
 // 用积分图像计算 16 个箱子的直方图
 histogram= intHistogram(x,y,width,height);
 // 计算与基准直方图的差距
 double distance= cv::compareHist(refHistogram,
 histogram,
 CV_COMP_INTERSECT);
 // 找到最相似直方图的位置
 if (distance>maxSimilarity) {
 xbest= x;
 ybest= y;
 maxSimilarity= distance;
 }
 }
}
// 在最准确的位置画矩形
cv::rectangle(secondImage, cv::Rect(xbest,ybest,width,height),0)); 

然后就可确定直方图最相似的位置,如下图所示。

20201227193907521 - OpenCV计算机视觉编程篇四《用直方图统计像素》

白色矩形表示搜索的区域。计算区域内部所有窗口的直方图。这里的窗口尺寸是固定的,但 是更好的做法是也搜索稍小或稍大的窗口,以便应对缩放比例可能带来的变动。有一点需要注意, 为了降低计算复杂度,要减少直方图中要计算的箱子数量。本例减少到 16 个箱子。因此,在这 个多平面图像中,平面 0 包含一个二值图像,表示值从 0 到 15 的所有像素;平面 1 表示值从 16 到 31 的全部像素,等等。

对物体的搜索过程包含了用预定范围的像素,计算指定尺寸的所有窗口的直方图的计算过 程,这意味着从积分图像对 3200 个直方图进行了高效计算。IntegralImage 类返回的直方图 都存储在 cv::Vec 对象中(因为用了 at 方法)。然后用 cv::compareHist 函数找到最相似的 直方图(和大多数 OpenCV 函数一样,这个函数可以利用实用的通用参数类型 cv::InputArray 获得 cv::Mat 或 cv::Vec)。

4.8.4 参阅

  • 第 8 章将讲述 SURF 运算符,它也基于对积分图像的使用。
  • 14.3 节将介绍如何用积分图像计算 Haar 特征。
  • 5.4 节将介绍另一种运算符,所得结果与前面介绍的自适应阈值化算法非常接近。
  • A. Adam、E. Rivlin 和 I. Shimshoni 于 2006 年发表在 Proceedings of the Int. Conference on Computer Vision and Pattern Recognition第798页至第805页的文章“Robust Fragments-based Tracking using the Integral Histogram”介绍了一种有趣的方法,利用积分图像在一个图像 队列中跟踪物体。

转载请注明:虚坏叔叔 » OpenCV计算机视觉编程篇四《用直方图统计像素》

喜欢 (3)

您必须 登录 才能发表评论!