一、直方图均衡化
【实现】
#include
#include
#include
#include
using namespace std;
using namespace cv;
int main()
{
//读取输入的图像
cv::Mat image = cv::imread("bluesky.jpg",0);
cv::Mat result;
cv::equalizeHist(image, result);
cv::namedWindow("Equalized Image");
cv::imshow("Equalized Image", result);
cv::namedWindow("Original Image");
cv::imshow("Original Image", image);
cv::waitKey(0);
}
【实现原理】
在一个完全均衡的直方图中,所有箱子所包含的像素数量是相等的。这意味着 50%像素的强度值小于128(强度中值),25%像素的强度值小于64,以此类推。这个现象可以用一条规则来表示:p%像素的强度值必须小于或等于255*p%。这条规则用于直方图均衡化处理,表示强度值i的映像对应强度值小于i的像素所占的百分比。因此可以用下面的语句构建所需的查找表:
lookup.at(i)= static_cast(255.0*p[i]/image.total());
这里的 p[i]是强度值小于或等于 i 的像素数量,通常称为累计直方图。这种直方图包含小于或等于指定强度值的像素数量,而非仅仅包含等于指定强度值的像素数量。前面说过image.total()返回图像的像素总数,因此 p[i]/image.total()就是像素数量的百分比。
一般来说,直方图均衡化会大大改进图像外观,但是改进的效果会因图像可视内容的不同而不同。
二、反向投影直方图检测特定图像内容
直方图是图像内容的一个重要特性。如果图像的某个区域含有特定的纹理或物体,这个区域 的直方图就可以看作一个函数,该函数返回某个像素属于这个特殊纹理或物体的概率。本节将介 绍如何运用直方图反向投影的概念方便地检测特定的图像内容。
【实现】
假设你希望在某幅图像中检测出特定的内容(例如检测出下图中天上的云彩),首先要做的 就是选择一个包含所需样本的感兴趣区域。接着提取该 ROI 的直方图,归一化直方图,从归一化后的直方图中读取概率值并把输入图像中的每个像素替换成与之对应的概率值。得到的结果就是下面的概率分布图。为提高可读性,可以对图像做了反色处理(下面代码没有进行反色处理),属于该区域的概率从亮(低概率)到暗(高概率)。最后,对此图做阈值化处理,就能得到最有可能是“云彩”的像素。
#include
#include
#include
#include
using namespace std;
using namespace cv;
//创建灰度图像的直方图
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 getHistogramImage(const cv::Mat& image, int zoom = 1);
static cv::Mat getImageOfHistogram(const cv::Mat& hist, int zoom);
cv::Mat getBackprojection(const cv::Mat& image);
};
//计算一维直方图
cv::Mat Histogram1D::getHistogram(const cv::Mat& image) {
cv::Mat hist;
//用calcHist函数计算一维直方图
cv::calcHist(&image, 1, //仅为一幅图像的直方图
channels, //使用的通道
cv::Mat(), //不使用掩码
hist, //作为结果的直方图
1, //这是一维的直方图
histSize, //箱子数量
ranges //像素值的范围
);
return hist;
}
//创建一个表示直方图的图像(静态方法)
cv::Mat Histogram1D::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(0.9 * histSize);
//为每个箱子画垂直线
for (int h = 0; h < histSize; h++) {
float binVal = hist.at(h);
if (binVal > 0) {
int intensity = static_cast(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;
}
//计算一维直方图,并返回它的图像
cv::Mat Histogram1D::getHistogramImage(const cv::Mat& image, int zoom) {
zoom = 1;
//先计算直方图
cv::Mat hist = getHistogram(image);
//创建图像
return getImageOfHistogram(hist, zoom);
}
cv::Mat Histogram1D::getBackprojection(const cv::Mat& image) {
cv::Mat result;
cv::Mat hist = getHistogram(image);
//归一化直方图可得到一个函数,由此可得到特定强度值的像素属于这个区域的概率
cv::normalize(hist, hist, 1.0);
//从归一化后的直方图中读取概率值并把输入图像中的每个像素替换成与之对应的概率值。
cv::calcBackProject(&image,
1,
channels,
hist,
result,
ranges,
255.0
);
return result;
}
int main()
{
//读取输入的图像
cv::Mat image = cv::imread("beach.jpg",0);
cv::Mat result;
//提取感兴趣区域
cv::Mat imageROI;
imageROI = image(cv::Rect(567, 83, 30, 24));//云彩区域
//获得ROI直方图
Histogram1D h;
cv::Mat hist = h.getHistogram(imageROI);
result = h.getBackprojection(image);
//阈值化处理,得到可能是云彩的图像
cv::threshold(result, result, 40, 255, cv::THRESH_BINARY);
cv::namedWindow("Backprojection result");
cv::imshow("Backprojection result", result);
cv::waitKey(0);
}
【实现原理】
前面的结果并不令人满意。因为除了云彩,其他区域也被错误地检测到了。这个概率函数是 从一个简单的灰度直方图提取的,理解这点很重要。很多其他像素的强度值与云彩像素的强度值 是相同的,在对直方图进行反向投影时会用相同的概率值替换具有相同强度值的像素。有一种方 案可提高检测效果,那就是使用色彩信息。要实现这点,需改变对 cv::calBackProject 的调用方式,扩展阅读将详细介绍这个函数。
cv::calBackProject 函数和 cv::calcHist 有些类似。一个像素的值对应直方图的一个箱子(可能是多维的)。但 cv::calBackProject 不会增加箱子的计数,而是把从箱子读取的值赋给反向投影图像中对应的像素。函数的第一个参数指明输入的图像(通常只有一个),接着需要指明使用的通道数量。这里传递给函数的直方图是一个输入参数,它的维度数要与通道列表数组的维度数一致。与 cv::calcHist 函数一样,这里的 ranges 参数用数组形式指定了输入直方图的箱子边界。该数组以浮点数组为元素,每个数组元素表示一个通道的取值范围(最小值和最大值)。
输出结果是一幅图像,包含计算得到的概率分布图。由于每个像素已经被替换成直方图中对应箱子处的值,因此输出图像的值范围是 0.0~1.0(假定输入的是归一化直方图)。最后一个参数是换算系数,可用来重新缩放这些值。
【扩展阅读】
现在来学习如何在直方图反向映射算法中使用色彩信息。
多维度直方图也可以在图像上进行反向映射。我们定义一个封装反向映射过程的类,首先定 义必需的参数并初始化。
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; ichannels[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);
通常来说,采用 BGR 色彩空间识别图像中的彩色物体并不是最好的方法。为了提高可靠性,我们在计算直方图之前减少了颜色的数量(要知道原始 BGR 色彩空间有超过 1600 万种颜色)。提取的直方图代表了天空区域的典型颜色分布情况。用它在其他图像上反向投影,也能检测到天空区域。注意,用多个天空图像构建直方图可以提高检测的准确性。
本例中,计算稀疏直方图可以减少内存使用量。你可以使用 cv::SparseMat 重做该实验。另外,如果要寻找色彩鲜艳的物体,使用 HSV 色彩空间的色调通道可能会更加有效。在其他情况下,最好使用感知上均匀的色彩空间(例如 L*a*b*)的色度组件。
【问题】
扩展阅读中的代码我无法整合实现。
三、用均值平移算法查找目标
直方图反向投影的结果是一个概率分布图,表示一个指定图像片段出现在特定位置的概率。如果我们已经知道图像中某个物体的大致位置,就可以用概率分布图找到物体的准确位置。窗口中概率最大的位置就是物体最可能出现的位置。因此,我们可以从一个初始位置开始,在周围反复移动以提高局部匹配概率,也许就能找到物体的准确位置。这个实现方法称为均值平移算法。
【实现】
#include
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
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
}
cv::Mat getHueHistogram(const cv::Mat& image, int minSaturation = 0);
};
// 计算一维色调直方图
// BGR 的原图转换成 HSV
// 忽略低饱和度的像素
cv::Mat ColorHistogram::getHueHistogram(const cv::Mat& image, int minSaturation) {
cv::Mat hist;
// 转换成 HSV 色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, CV_BGR2HSV);
// 掩码(可能用到,也可能用不到)
cv::Mat mask;
// 根据需要创建掩码
if (minSaturation > 0) {
// 将 3 个通道分割进 3 幅图像
std::vector 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;
}
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);
cv::Mat find(const cv::Mat& image);
cv::Mat find(const cv::Mat& image, float minValue, float maxValue, int* channels);
};
// 设置引用的直方图
void ContentFinder::setHistogram(const cv::Mat& h) {
histogram = h;
cv::normalize(histogram, histogram, 1.0);
}
// 使用全部通道,范围[0,256]
cv::Mat ContentFinder::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 ContentFinder::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;
}
int main()
{
cv::Mat image;
image = cv::imread("animal1.jpg");
//猩猩脸部的ROI
cv::Rect rect1(185, 48, 50, 65);
cv::Mat imageROI = image(rect1);
//得到猩猩脸部直方图
int minSat = 65;
ColorHistogram hc;
cv::Mat colorhist = hc.getHueHistogram(imageROI, minSat);
ContentFinder finder;
finder.setHistogram(colorhist);
image = cv::imread("baboon2.jpg");
// 转换成 HSV 色彩空间
cv::Mat 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);
// 窗口初始位置
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);
}
实现失败,我也不知道为啥错了……
【实现原理】
本例为了突出被寻找物体的特征,使用了 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 函数返回已经执行的迭代次数。显然,结果的好坏取决于指定初始位置提供的概率分布图的质量。注意,这里用颜色直方图表示图像的外观。也可以用其他特征的直方图(例如边界方向直方图)来表示物体。