OpenCV实战(9)——基于反向投影直方图检测图像内容
- 0. 前言
- 1 反向投影直方图
- 2. 反向投影颜色直方图
- 3. 完整代码
- 小结
- 系列链接
0. 前言
直方图是图像内容的一个重要特征。如果查看显示特定纹理或特定对象的图像区域,则该区域的直方图可以看作是一个函数,该函数给出了给定像素属于特定纹理或对象的概率。在本节中,将介绍直方图反投影的概念,以及如何将其用于检测特定的图像内容。
1 反向投影直方图
(1) 假设我们希望检测图像中的特定内容,例如,图中天空的云彩。首先选择一个包含目标对象的感兴趣区域 (Region of Interest
, ROI
),在图像中使用矩形框绘制此区域:
// 定义 ROI
cv::Mat imageROI;
imageROI = image(cv::Rect(200, 33, 24, 30));
(2) 使用 Histogram1D
类计算此 ROI
的直方图:
Histogram1D h;
cv::Mat hist = h.getHistogram(imageROI);
(3) 通过对这个直方图进行归一化,得到一个函数,该函数给出了给定强度值的像素属于定义区域的概率:
cv::normalize(h, histogram, 1.0);
(4) 反向投影直方图包括将输入图像中的每个像素值替换为其在归一化直方图中读取的相应概率值。OpenCV
使用 cv::calcBackProject()
函数执行此任务:
cv::calcBackProject(
&image,
1, // 一次仅使用一张图片
channels, // 指定直方图维度
histogram, // 所用直方图
result, // 结果方向投影图像
ranges, // 值范围
255.0 // 缩放因子
);
使用以上函数可以得到以下概率图,属于 ROI
的概率从亮(低概率)到暗(高概率):
(5) 如果我们对该图像应用阈值,我们可以获得最可能的云像素:
cv::threshold(result, result, threshold, 255.0, cv::THRESH_BINARY);
结果如下图所示:
上图结果并不令人十分满意,因为除了云之外,许多其他区域也会被错误地检测,但本节最重要的目的是要理解概率函数是从简单直方图中提取的。图像中的许多其他像素与云像素具有相同的强度,并且在反向投影直方图中将相同强度的像素替换为相同的概率值。改善检测结果的一种解决方案是使用颜色信息,为此,我们需要修改对 cv::calBackProject
的调用。
cv::calBackProject
函数类似于 cv::calcHist
函数。第一个参数指定输入图像;然后需要列出使用的通道索引,传递给函数的直方图作为一个输入参数;维度应该与通道列表数组之一匹配;与 cv::calcHist
一样,ranges
参数以浮点数组的形式指定输入直方图的 bin
边界,每个数组指定每个通道的范围(最小值和最大值);输出结果 result
是计算出的概率图。由于每个像素都被在对应 bin
位置的直方图中找到的值替换,因此生成的图像的值介于 0.0
和 1.0
之间(假设已提供归一化直方图作为输入);最后一个参数用于通过将这些值乘以给定因子来重新调整这些值。
2. 反向投影颜色直方图
(1) 多维直方图也可以反向投影到图像上,我们需要定义一个封装反向投影过程的类。首先,我们定义所需的属性并初始化数据:
class ContentFinder {
private:
// 直方图参数
float hranges[2];
const float* ranges[3];
int channels[3];
float threshold;
cv::Mat histogram;
cv::SparseMat shistogram;
bool isSparse;
public:
ContentFinder() : threshold(0.01f), isSparse(false) {
ranges[0] = hranges;
ranges[1] = hranges;
ranges[2] = hranges;
}
(2) 接下来,我们定义一个阈值参数,用于创建显示检测结果的二值图像。如果此参数设置为负值,则将返回原始概率图:
// 设置阈值
void setThreshold(float t) {
threshold = t;
}
// 获取阈值
float getThreshold() {
return threshold;
}
(3) 归一化输入直方图:
// 设置参考直方图
void setHistogram(const cv::Mat& h) {
cv::normalize(h, histogram, 1.0);
}
(4) 要反向投影直方图,只需指定图像、范围(假设所有通道具有相同的范围)和使用的通道列表:
cv::Mat find(const cv::Mat& image) {
cv::Mat result;
hranges[0] = 0.0;
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
);
if (threshold>0.0) {
cv::threshold(result, result, 255.0*threshold, 255.0, cv::THRESH_BINARY);
}
return result;
}
(5) 使用 BGR
直方图尝试检测蓝天区域。我们将首先加载彩色图像,定义感兴趣的区域,并在减色后的色彩空间中计算 3D
直方图:
// 加载彩色图像
ColorHistogram hc;
cv::Mat color = cv::imread("2.png");
imageROI = color(cv::Rect(0, 0, 100, 45));
// 获取 3D 直方图
hc.setSize(8);
cv::Mat shist = hc.getHistogram(imageROI);
(5) 接下来,需要计算直方图并使用 find
方法检测图像的天空部分:
// 创建内容检测器
ContentFinder finder;
finder.setHistogram(shist);
finder.setThreshold(0.05f);
result1 = finder.find(color);
对彩色图像的检测结果如下所示:
BGR
颜色空间通常不是识别图像中颜色对象的最佳颜色空间。为了便于计算,在计算直方图之前减少了颜色数量,提取的直方图表示天空区域的典型颜色分布。尝试将其反投影到另一个图像上,它同样可以检测天空部分。使用由多个天空图像构建的直方图可以提高检测的准确性。
在这种情况下,计算稀疏直方图会降低内存使用,我们可以尝试使用 cv::SparseMat
完成此任务。此外,如果我们需要检测具有鲜艳颜色的对象,使用 HSV
颜色空间的色调通道会更有效。在其他情况下,使用感知均匀空间(例如 L*a*b*
)的色度分量可能是更好的选择。
3. 完整代码
头文件 colorhistogram.h
如下所示:
#if !defined COLHISTOGRAM
#define COLHISTOGRAM
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
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;
hranges[1] = 256.0;
ranges[0] = hranges;
ranges[1] = hranges;
ranges[2] = hranges;
channels[0] = 0;
channels[1] = 1;
channels[2] = 2;
}
// 设置直方图尺寸
void setSize(int size) {
histSize[0] = histSize[1] = histSize[2] = size;
}
cv::Mat getHistogram(const cv::Mat &image) {
// 计算直方图
cv::Mat hist;
hranges[0] = 0.0;
hranges[1] = 256.0;
channels[0] = 0;
channels[1] = 1;
channels[2] = 2;
cv::calcHist(&image,
1, // 使用一张图片计算直方图
channels, // 使用的通道
cv::Mat(), // 不使用掩码
hist, // 结果
3, // 3D 直方图
histSize, // bins 数量
ranges // 像素值范围
);
return hist;
}
// 计算直方图
cv::SparseMat getSparseHistogram(const cv::Mat& image) {
cv::SparseMat hist(3, histSize, CV_32F);
hranges[0] = 0.0;
hranges[1] = 256.0;
channels[0] = 0;
channels[1] = 1;
channels[2] = 2;
// 计算直方图
cv:: calcHist(
&image,
1,
channels,
cv::Mat(),
hist,
3,
histSize,
ranges
);
return hist;
}
// 计算 1D Hue 直方图
cv::Mat getHueHistogram(const cv::Mat& image, int minSaturation=0) {
cv::Mat hist;
// 转换为 HSV 色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
cv::Mat mask;
if (minSaturation>0) {
std::vector<cv::Mat> v;
cv::split(hsv, v);
cv::threshold(v[1], mask, minSaturation, 255, cv::THRESH_BINARY);
}
hranges[0] = 0.0;
hranges[1] = 180.0;
channels[0] = 0;
// 计算直方图
cv::calcHist(
&hsv,
1,
channels,
mask,
hist,
1,
histSize,
ranges
);
return hist;
}
// 计算 2D ab 直方图
cv::Mat getabHistogram(const cv::Mat& image) {
cv::Mat hist;
// 转换为 Lab 色彩空间
cv::Mat lab;
cv::cvtColor(image, lab, cv::COLOR_BGR2Lab);
hranges[0] = 0;
hranges[1] = 256.0;
channels[0] = 1;
channels[1] = 2;
// 计算直方图
cv::calcHist(
&lab,
1,
channels,
cv::Mat(),
hist,
2,
histSize,
ranges
);
return hist;
}
};
#endif
头文件 contentFinder.h
如下所示:
#if !defined OFINDER
#define OFINDER
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
class ContentFinder {
private:
// 直方图参数
float hranges[2];
const float* ranges[3];
int channels[3];
float threshold;
cv::Mat histogram;
cv::SparseMat shistogram;
bool isSparse;
public:
ContentFinder() : threshold(0.01f), isSparse(false) {
ranges[0] = hranges;
ranges[1] = hranges;
ranges[2] = hranges;
}
// 设置阈值
void setThreshold(float t) {
threshold = t;
}
// 获取阈值
float getThreshold() {
return threshold;
}
// 设置参考直方图
void setHistogram(const cv::Mat& h) {
isSparse = false;
cv::normalize(h, histogram, 1.0);
}
void setHistogram(const cv::SparseMat& h) {
isSparse = true;
cv::normalize(h, shistogram, 1.0, cv::NORM_L2);
}
cv::Mat find(const cv::Mat& image) {
cv::Mat result;
hranges[0] = 0.0;
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;
if (isSparse) {
for (int i=0; i<shistogram.dims(); i++) {
this->channels[i] = channels[i];
}
cv::calcBackProject(
&image,
1, // 一次仅使用一张图片
channels, // 指定直方图维度
shistogram, // 所用直方图
result, // 结果方向投影图像
ranges, // 值范围
255.0 // 缩放因子
);
} else {
for (int i=0; i<histogram.dims; i++) {
this->channels[i] = channels[i];
}
cv::calcBackProject(
&image,
1,
channels,
histogram,
result,
ranges,
255.0
);
}
if (threshold>0.0) {
cv::threshold(result, result, 255.0*threshold, 255.0, cv::THRESH_BINARY);
}
return result;
}
};
#endif
主函数代码 contentFinder.cpp
如下所示:
#include <iostream>
using namespace std;
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "histogram.h"
#include "contentFinder.h"
#include "colorhistogram.h"
int main() {
// 读取输入图像
cv::Mat image = cv::imread("example.jpeg",0);
if (!image.data) return 0;
// 定义 ROI
cv::Mat imageROI;
imageROI = image(cv::Rect(1000, 10, 120, 120));
// 显示参考块
cv::namedWindow("Reference");
cv::imshow("Reference", imageROI);
// 计算参考直方图
Histogram1D h;
cv::Mat hist = h.getHistogram(imageROI);
cv::namedWindow("Reference Hist");
cv::imshow("Reference Hist", h.getHistogramImage(imageROI));
// 创建内容检测器
ContentFinder finder;
// 反向投影直方图
finder.setHistogram(hist);
finder.setThreshold(-1.0f);
cv::Mat result1;
result1 = finder.find(image);
cv::Mat tmp;
result1.convertTo(tmp, CV_8U, -1.0, 255.0);
cv::namedWindow("Backprojection result");
cv::imshow("Backprojection result", tmp);
// 获取二值反向投影
finder.setThreshold(0.12f);
result1 = finder.find(image);
cv::rectangle(image, cv::Rect(1000, 10, 120, 120), cv::Scalar(0, 0, 0));
cv::namedWindow("Image");
cv::imshow("Image", image);
cv::namedWindow("Detection Result");
cv::imshow("Detection Result", result1);
// 加载彩色图像
ColorHistogram hc;
cv::Mat color = cv::imread("example.jpeg");
imageROI = color(cv::Rect(1000, 10, 120, 120));
// 获取 3D 直方图
hc.setSize(8);
cv::Mat shist = hc.getHistogram(imageROI);
finder.setHistogram(shist);
finder.setThreshold(0.05f);
result1 = finder.find(color);
cv::namedWindow("Color Detection Result");
cv::imshow("Color Detection Result", result1);
// 第二张彩色图像
cv::Mat color2 = cv::imread("example_2.jpeg");
cv::namedWindow("Second Image");
cv::imshow("Second Image", color2);
// 反向投影直方图
cv::Mat result2 = finder.find(color2);
cv::namedWindow("Result color (2)");
cv::imshow("Result color (2)", result2);
// 计算 ab 色彩直方图
hc.setSize(256);
cv::Mat colorhist = hc.getabHistogram(imageROI);
colorhist.convertTo(tmp, CV_8U, -1.0, 255.0);
cv::namedWindow("ab histogram");
cv::imshow("ab histogram", tmp);
// 反向投影
finder.setHistogram(colorhist);
finder.setThreshold(0.05f);
cv::Mat lab;
cv::cvtColor(color, lab, cv::COLOR_BGR2Lab);
int ch[2] = {1, 2};
result1 = finder.find(lab, 0, 256.0f, ch);
cv::namedWindow("Result ab (1)");
cv::imshow("Result ab (1)", result1);
// 第二张图像
cv::cvtColor(color2, lab, cv::COLOR_BGR2Lab);
result2 = finder.find(lab, 0, 256.0, ch);
cv::namedWindow("Result ab (2)");
cv::imshow("Result ab (2)", result2);
// 为参考区域绘制矩形框
cv::rectangle(color, cv::Rect(0, 0, 100, 45), cv::Scalar(0, 0, 0));
cv::namedWindow("Color image");
cv::imshow("Color image", color);
// 计算 Hue 直方图
hc.setSize(180);
colorhist = hc.getHueHistogram(imageROI);
// 反向投影
finder.setHistogram(colorhist);
// 转换色彩空间
cv::Mat hsv;
cv::cvtColor(color, hsv, cv::COLOR_BGR2HSV);
// hue 直方图反向投影
ch[0] = 0;
result2 = finder.find(hsv, 0.0f, 180.0f, ch);
cv::namedWindow("Result Hue (1)");
cv::imshow("Result Hue (1)", result1);
// 第二张图像
color2 = cv::imread("3.png");
cv::cvtColor(color2, hsv, cv::COLOR_BGR2HSV);
result2 = finder.find(hsv, 0.0f, 180.0f, ch);
cv::namedWindow("Result Hue (2)");
cv::imshow("Result Hue (2)", result2);
cv::waitKey();
return 0;
}
小结
直方图是图像内容的一个重要特征,在许多场景中都有着重要用途。本节中,介绍了直方图反向投影的概念,以及如何将其用于检测特定的图像内容。
系列链接
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解