颜色直方图是一种常见的图像特征,顾名思义颜色直方图就是用来反映图像颜色组成分布的直方图。颜色直方图的横轴表示像素值或像素值范围,纵轴表示该像素值范围内像素点的个数或出现频率。颜色直方图属于计算机视觉中的基础概念,其常常被应用于图像相似度计算,视觉词袋,图像颜色平衡等。颜色直方图可以基于不同的颜色空间和坐标系来实现,本文主要基于RGB颜色空间和直角坐标系计算颜色直方图。
颜色直方图是图像的一种全局颜色特征,优点为方法简单、计算迅速、对旋转和尺度等变化不敏感,缺点是忽略了图像的空间分布信息以及用于相似度对比时往往不那么准确。当然对于颜色直方图有一些改进的变种算法,但是本文只介绍最原始的颜色直方图计算方法。因为改进过的算法提效不高,还不如直接用深度学习。本文主要内容有:颜色直方图的计算、图像均衡化、直方图比较和反向投影,涉及到用于直方图计算的OpenCV函数出自OpenCV_Histograms。
文章目录
- 1 颜色直方图的计算
- 2 图像均衡化
- 3 直方图比较
- 4 反向投影
- 5 参考
本文所有代码见:
- github: OpenCV-Practical-Exercise
- gitee(备份,主要是下载速度快): OpenCV-Practical-Exercise-gitee
1 颜色直方图的计算
opencv使用内置calcHist函数计算图像的颜色直方图,calcHist函数c++接口如下,python接口类似。
void cv::calcHist(const Mat * images, int nimages, const int * channels,
InputArray mask, OutputArray hist, int dims, const int * histSize,
const float ** ranges, bool uniform = true, bool accumulate = false);
函数说明如下:
- images:输入的图像;
- nimages:输入图像数;
- channels:用输入图像的第几个颜色通道进行计算;
- mask:掩模,掩膜的作用为只计算图片中某一区域的直方图,而忽略其他区域;
- hist:直方图输出结果;
- dims:输出直方图的维度;
- histSize:直方图像素值范围分为多少区间(直方图条形个数);
- ranges:直方图像素值统计范围;
- uniform=true:是否对得到的直方图数组进行归一化处理;
- accumulate=false:当输入多个图像时,是否累积计算像素值的个数;
通过calcHist函数能够计算出每个像素范围下的像素点个数,其中histSize表示有多少个像素点区间。比如像素值范围为0到255,如果histSize设置为256,则表示每一个像素值区间跨度为1。如果histSize设置为128,表示每一个像素值区间跨度为256/128=2。以下代码展示了calcHist函数使用方法,分为calcHist计算和结果绘图。结果绘图代码看着很复杂,因为OpenCV绘图功能很一般。可以通过其他的方式绘制图片。
C++
#include "opencv2/highgui.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
auto imgpath = "image/lena.jpg";
// 读取彩色图片
Mat src = imread(imgpath, IMREAD_COLOR);
if (src.empty())
{
return -1;
}
vector<Mat> bgr_planes;
// 图像RGB颜色通道分离
split(src, bgr_planes);
// 将直方图像素值分为多少个区间/直方图有多少根柱子
int histSize = 256;
// 256不会被使用
float range[] = { 0, 256 };
const float* histRange = { range };
// 一些默认参数,一般不变
bool uniform = true, accumulate = false;
Mat b_hist, g_hist, r_hist;
// 参数依次为:
// 输入图像: &bgr_planes[0]
// 输入图像个数:1
// 使用输入图像的第几个通道:0
// 掩膜:Mat()
// 直方图计算结果:b_hist,b_hist存储histSize个区间的像素值个数
// 直方图维度:1
// 直方图像素值范围分为多少区间(直方图条形个数):256
// 是否对得到的直方图数组进行归一化处理;uniform
// 当输入多个图像时,是否累积计算像素值的个数accumulate
calcHist(&bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histSize, &histRange, uniform, accumulate);
calcHist(&bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRange, uniform, accumulate);
calcHist(&bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, &histRange, uniform, accumulate);
// b_hist表示每个像素范围的像素值个数,其总和等于输入图像长乘宽。
// 如果要统计每个像素范围的像素值百分比,计算方式如下
// b_hist /= (float)(cv::sum(b_hist)[0]);
// g_hist /= (float)(cv::sum(g_hist)[0]);
// r_hist /= (float)(cv::sum(r_hist)[0]);
/* 以下的参数都是跟直方图展示有关,c++展示图片不那么容易*/
// 一些绘图参数
int hist_w = 512, hist_h = 400;
int bin_w = cvRound((double)hist_w / histSize);
// 创建一张黑色背景图像,用于展示直方图绘制结果
Mat histImage(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0));
// 将直方图归一化到0到histImage.rows,最后两个参数默认就好。
normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());
normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());
normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());
for (int i = 1; i < histSize; i++)
{
//遍历hist元素(注意hist中是float类型)
// 绘制蓝色分量
line(histImage, Point(bin_w*(i - 1), hist_h - cvRound(b_hist.at<float>(i - 1))),
Point(bin_w*(i), hist_h - cvRound(b_hist.at<float>(i))),
Scalar(255, 0, 0), 2, 8, 0);
// 绘制绿色分量
line(histImage, Point(bin_w*(i - 1), hist_h - cvRound(g_hist.at<float>(i - 1))),
Point(bin_w*(i), hist_h - cvRound(g_hist.at<float>(i))),
Scalar(0, 255, 0), 2, 8, 0);
// 绘制红色分量
line(histImage, Point(bin_w*(i - 1), hist_h - cvRound(r_hist.at<float>(i - 1))),
Point(bin_w*(i), hist_h - cvRound(r_hist.at<float>(i))),
Scalar(0, 0, 255), 2, 8, 0);
}
imshow("src image", src);
imshow("dst image", histImage);
waitKey(0);
destroyAllWindows();
return 0;
}
Python
import cv2
import numpy as np
def main():
imgpath = "image/lena.jpg"
src = cv2.imread(imgpath)
if src is None:
print('Could not open or find the image:', imgpath)
return -1
bgr_planes = cv2.split(src)
histSize = 256
# 256会被排除
histRange = (0, 256)
accumulate = False
b_hist = cv2.calcHist(bgr_planes, [0], None, [
histSize], histRange, accumulate=accumulate)
g_hist = cv2.calcHist(bgr_planes, [1], None, [
histSize], histRange, accumulate=accumulate)
r_hist = cv2.calcHist(bgr_planes, [2], None, [
histSize], histRange, accumulate=accumulate)
# b_hist表示每个像素范围的像素值个数,其总和等于输入图像长乘宽。
# 如果要统计每个像素范围的像素值百分比,计算方式如下
assert(sum(b_hist) == src.shape[0] *src.shape[1])
# b_hist /= sum(b_hist)
# g_hist /= sum(g_hist)
# r_hist /= sum(r_hist)
# assert(sum(b_hist) == 1)
# 以下是绘图代码
hist_w = 512
hist_h = 400
bin_w = int(round(hist_w/histSize))
histImage = np.zeros((hist_h, hist_w, 3), dtype=np.uint8)
cv2.normalize(b_hist, b_hist, alpha=0, beta=hist_h,
norm_type=cv2.NORM_MINMAX)
cv2.normalize(g_hist, g_hist, alpha=0, beta=hist_h,
norm_type=cv2.NORM_MINMAX)
cv2.normalize(r_hist, r_hist, alpha=0, beta=hist_h,
norm_type=cv2.NORM_MINMAX)
for i in range(1, histSize):
cv2.line(histImage, (bin_w*(i-1), hist_h - int(np.round(b_hist[i-1]))),
(bin_w*(i), hist_h - int(np.round(b_hist[i]))),
(255, 0, 0), thickness=2)
cv2.line(histImage, (bin_w*(i-1), hist_h - int(np.round(g_hist[i-1]))),
(bin_w*(i), hist_h - int(np.round(g_hist[i]))),
(0, 255, 0), thickness=2)
cv2.line(histImage, (bin_w*(i-1), hist_h - int(np.round(r_hist[i-1]))),
(bin_w*(i), hist_h - int(np.round(r_hist[i]))),
(0, 0, 255), thickness=2)
cv2.imshow('src image', src)
cv2.imshow('dst image', histImage)
cv2.waitKey(0)
cv2.destroyAllWindows()
return 0
if __name__ == "__main__":
main()
结果如下所示,展示了图片每一个通道的颜色信息。如果是输入是灰度图,稍微修改下代码即可。
类型 | 颜色直方图 |
---|---|
输入图片 | |
输出图片 |
2 图像均衡化
图像均衡化是一种提高图像对比度的方法,通过变换函数将原图像的直方图修正为分布比较均匀的直方图,从而改变图像整体偏暗或整体偏亮,灰度层次不丰富的情况。图像均衡化的具体原理见:直方图均衡化详解。在OpenCV中提供equalizeHist函数实现直方图的均衡化,但是equalizeHist函数只对灰度图进行运算。
以下代码展示了equalizeHist函数的使用。
C++
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
auto imgpath = "image/lena.jpg";
// 读取彩色图片
Mat src = imread(imgpath, IMREAD_COLOR);
if (src.empty())
{
return -1;
}
// 变为灰度图
cvtColor(src, src, COLOR_BGR2GRAY);
Mat dst;
equalizeHist(src, dst);
imshow("src image", src);
imshow("dst Image", dst);
waitKey(0);
destroyAllWindows();
return 0;
}
Python
import cv2
def main():
imgpath = "image/lena.jpg"
src = cv2.imread(imgpath)
if src is None:
print('Could not open or find the image:', imgpath)
return -1
src = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
dst = cv2.equalizeHist(src)
cv2.imshow("src image", src)
cv2.imshow("dst image", dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
return 0
if __name__ == "__main__":
main()
结果如下所示,可以看到直方图均衡的作用是扩大颜色直方图像素区间的分布范围,使得分布更加均匀。
类型 | 图片 | 颜色直方图 |
---|---|---|
输入图片 | ||
直方图均衡 | ||
自适应直方图均衡 |
但是标准的直方图均衡会导致图中部分区域由于对比度增强过大而成为噪点;或导致一些区域调整后变得更暗/更亮而丢失细节信息。所以面对这种情况,OpenCV提供自适应直方图均衡以获得更好的结果。
自适应直方图均衡的工作原理是将图像划分为MxN个网格,然后将直方图均衡局部应用于每个网格,同时设置对比度限制阈值。结果是输出图像总体上具有更高的对比度(理想情况下)并抑制噪声。OpenCV实现自适应直方图的代码结果如上所示,可以看到直方图分布更加平滑。自适应直方图均衡缺点是效果很依靠手动调整参数(传统图像算法的通病),其具体原理见限制对比度自适应直方图均衡化算法原理、实现及效果,实现代码如下:
C++
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
auto imgpath = "image/lena.jpg";
// 读取彩色图片
Mat src = imread(imgpath, IMREAD_COLOR);
if (src.empty())
{
return -1;
}
// 变为灰度图
cvtColor(src, src, COLOR_BGR2GRAY);
Mat dst;
cv::Ptr<CLAHE> clahe = cv::createCLAHE();
// 设置对比度限制阈值
clahe->setClipLimit(2);
// 设置划分网格数量
clahe->setTilesGridSize(cv::Size(16, 16));
clahe->apply(src, dst);
imshow("src image", src);
imshow("dst Image", dst);
waitKey(0);
destroyAllWindows();
return 0;
}
Python
import cv2
def main():
imgpath = "image/lena.jpg"
src = cv2.imread(imgpath)
if src is None:
print('Could not open or find the image:', imgpath)
return -1
src = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
clahe = cv2.createCLAHE(clipLimit=2, tileGridSize=(16, 16))
dst = clahe.apply(src)
cv2.imshow("src image", src)
cv2.imshow("dst image", dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
return 0
if __name__ == "__main__":
main()
如果想对彩色图进行直方图均衡化,则有两种办法:1)分别对RGB三通道均衡化,再组合通道图输出结果;2)将图像颜色空间转化为YUV,YCbCr等颜色空间,仅对亮度通道进行均衡化,最后组合通道图并转回RGB空间。在这里推荐使用第二种办法,具体原因看下面示例代码的结果。所用的转换颜色空间是YUV颜色空间,想要进一步了解YUV颜色空间见YUV图像处理入门1及其他颜色空间见OpenCV中的颜色空间。
#include "opencv2/highgui.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
using namespace std;
using namespace cv;
// 颜色通道分别进行均衡化
Mat equalizeHistChannel(const Mat inputImage)
{
// 分离通道
vector<Mat> channels;
split(inputImage, channels);
// 各个通道图像进行直方图均衡
equalizeHist(channels[0], channels[0]);
equalizeHist(channels[1], channels[1]);
equalizeHist(channels[2], channels[2]);
// 合并结果
Mat result;
merge(channels, result);
return result;
}
// 仅对亮度通道进行均衡化
Mat equalizeHistIntensity(const Mat inputImage)
{
Mat yuv;
// 将bgr格式转换为yuv444
cvtColor(inputImage, yuv, COLOR_BGR2YUV);
vector<Mat> channels;
split(yuv, channels);
// 均衡化亮度通道
equalizeHist(channels[0], channels[0]);
Mat result;
merge(channels, yuv);
cvtColor(yuv, result, COLOR_YUV2BGR);
return result;
}
int main()
{
auto imgpath = "image/lena.jpg";
// 读取彩色图片
Mat src = imread(imgpath, IMREAD_COLOR);
if (src.empty())
{
return -1;
}
Mat dstChannel, dstIntensity;
dstChannel = equalizeHistChannel(src);
dstIntensity = equalizeHistIntensity(src);
imshow("src image", src);
imshow("dstChannel image", dstChannel);
imshow("dstIntensity image", dstIntensity);
waitKey(0);
destroyAllWindows();
return 0;
}
import cv2
# 颜色通道分别进行均衡化
def equalizeHistChannel(inputImage):
channels = cv2.split(inputImage)
# 各个通道图像进行直方图均衡
cv2.equalizeHist(channels[0], channels[0])
cv2.equalizeHist(channels[1], channels[1])
cv2.equalizeHist(channels[2], channels[2])
# 合并结果
result = cv2.merge(channels)
return result
# 仅对亮度通道进行均衡化
def equalizeHistIntensity(inputImage):
# 将bgr格式转换为yuv444
inputImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2YUV)
channels = cv2.split(inputImage)
# 均衡化亮度通道
cv2.equalizeHist(channels[0], channels[0])
# 合并结果
result = cv2.merge(channels)
result = cv2.cvtColor(result, cv2.COLOR_YUV2BGR)
return result
def main():
imgpath = "image/lena.jpg"
src = cv2.imread(imgpath)
if src is None:
print('Could not open or find the image:', imgpath)
return -1
dstChannel = equalizeHistChannel(src)
dstIntensity = equalizeHistIntensity(src)
cv2.imshow("src image", src)
cv2.imshow("dstChannel image", dstChannel)
cv2.imshow("dstIntensity image", dstIntensity)
cv2.waitKey(0)
cv2.destroyAllWindows()
return 0
if __name__ == "__main__":
main()
结果如下所示,可以看到颜色通道分别均衡化会导致最终合成的图片颜色失真,而仅对亮度通道均衡化则不会。这是因为R、G、B的值是表示亮度,通过对RGB的变化以及它们相互之间的叠加可以得到不同颜色。256级的RGB色彩能够组合约1678(2的24次方)万种色彩,通常简称为千万色或24位色。颜色均衡化是非线性过程,对RGB分别进行均衡化会产生不同的效应,最终导致合成的颜色出现变化。
类型 | 结果 |
---|---|
输入图片 | |
颜色通道分别均衡化 | |
仅对亮度通道均衡化 |
3 直方图比较
我们可以通过比较两幅图片的直方图来衡量两张图片之间的相似程度。OpenCV提供了compareHist函数来实现直方图的比较,也提供了多种直方图度量标准。这些度量标准的取值如下:
enum HistCompMethods {
HISTCMP_CORREL = 0, // 相关性比较
HISTCMP_CHISQR = 1, // 卡方比较
HISTCMP_INTERSECT = 2, // 交集比较
HISTCMP_BHATTACHARYYA = 3, // 巴氏距离
HISTCMP_HELLINGER = HISTCMP_BHATTACHARYYA, // 等同于巴氏距离
HISTCMP_CHISQR_ALT = 4, // 替代卡方:通常用于纹理比较。
HISTCMP_KL_DIV = 5 // KL散度
};
以上评价指标可以自行搜索查询相关含义,具体使用哪个评价指标好完全取决于数据集和目标,所以需要通过实验来确定效果最佳的指标。当然也可以自己设计评价指标,不过通过用直方图比较来衡量图片相似性本身效果不太好,各种评价指标都大差不差。直方图比较特点就是快,简单但是不太准。如果想了解其他基于图像处理算法的图片相似度比较方法可以参考基于图像哈希构建图像相似度对比算法。
下面的代码展示了compareHist函数的使用方式,代码综合hsv空间的h通道(色调)和s通道(饱和度)计算图像的颜色直方图。
C++
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
string imgs[] = { "image/lena.jpg", "image/lena_resize.jpg", "image/lena_flip.jpg","image/test.jpg" };
Mat src_base = imread(imgs[0]);
Mat src_test1 = imread(imgs[1]);
Mat src_test2 = imread(imgs[2]);
Mat src_test3 = imread(imgs[3]);
if (src_base.empty() || src_test1.empty() || src_test2.empty() || src_test3.empty())
{
cout << "Could not open or find the images!\n" << endl;
return -1;
}
// 将图片转换到hsv空间
Mat hsv_base, hsv_test1, hsv_test2, hsv_test3;
cvtColor(src_base, hsv_base, COLOR_BGR2HSV);
cvtColor(src_test1, hsv_test1, COLOR_BGR2HSV);
cvtColor(src_test2, hsv_test2, COLOR_BGR2HSV);
cvtColor(src_test3, hsv_test3, COLOR_BGR2HSV);
int h_bins = 50, s_bins = 60;
int histSize[] = { h_bins, s_bins };
// hue值变化范围为0到179,saturation值变化范围为0到255
float h_ranges[] = { 0, 180 };
float s_ranges[] = { 0, 256 };
const float* ranges[] = { h_ranges, s_ranges };
// 使用前两个通道计算直方图
int channels[] = { 0, 1 };
Mat hist_base, hist_half_down, hist_test1, hist_test2, hist_test3;
calcHist(&hsv_base, 1, channels, Mat(), hist_base, 2, histSize, ranges, true, false);
normalize(hist_base, hist_base, 0, 1, NORM_MINMAX, -1, Mat());
calcHist(&hsv_test1, 1, channels, Mat(), hist_test1, 2, histSize, ranges, true, false);
normalize(hist_test1, hist_test1, 0, 1, NORM_MINMAX, -1, Mat());
calcHist(&hsv_test2, 1, channels, Mat(), hist_test2, 2, histSize, ranges, true, false);
normalize(hist_test2, hist_test2, 0, 1, NORM_MINMAX, -1, Mat());
calcHist(&hsv_test3, 1, channels, Mat(), hist_test3, 2, histSize, ranges, true, false);
normalize(hist_test3, hist_test3, 0, 1, NORM_MINMAX, -1, Mat());
// 可以查看枚举变量HistCompMethods中有多少种compare_method方法;
for (int compare_method = 0; compare_method < 6; compare_method++)
{
// 不同方法的结果表示含义不一样
double base_base = compareHist(hist_base, hist_base, compare_method);
double base_test1 = compareHist(hist_base, hist_test1, compare_method);
double base_test2 = compareHist(hist_base, hist_test2, compare_method);
double base_test3 = compareHist(hist_base, hist_test3, compare_method);
printf("method[%d]: base_base : %.3f \t base_test1: %.3f \t base_test2: %.3f \t base_test3: %.3f \n", compare_method, base_base, base_test1, base_test2, base_test3);
}
printf("Done \n");
system("pause");
return 0;
}
Python
import cv2
def main():
imgs = ["image/lena.jpg", "image/lena_resize.jpg", "image/lena_flip.jpg","image/test.jpg"]
src_base = cv2.imread(imgs[0])
src_test1 = cv2.imread(imgs[1])
src_test2 = cv2.imread(imgs[2])
src_test3 = cv2.imread(imgs[3])
if src_base is None or src_test1 is None or src_test2 is None or src_test3 is None:
print('Could not open or find the images!')
exit(0)
# 将图片转换到hsv空间
hsv_base = cv2.cvtColor(src_base, cv2.COLOR_BGR2HSV)
hsv_test1 = cv2.cvtColor(src_test1, cv2.COLOR_BGR2HSV)
hsv_test2 = cv2.cvtColor(src_test2, cv2.COLOR_BGR2HSV)
hsv_test3 = cv2.cvtColor(src_test3, cv2.COLOR_BGR2HSV)
h_bins = 50
s_bins = 60
histSize = [h_bins, s_bins]
# hue值变化范围为0到179,saturation值变化范围为0到255
h_ranges = [0, 180]
s_ranges = [0, 256]
# 合并
ranges = h_ranges + s_ranges
# 使用前两个通道计算直方图
channels = [0, 1]
hist_base = cv2.calcHist([hsv_base], channels, None,
histSize, ranges, accumulate=False)
cv2.normalize(hist_base, hist_base, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX)
hist_test1 = cv2.calcHist([hsv_test1], channels, None,
histSize, ranges, accumulate=False)
cv2.normalize(hist_test1, hist_test1, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX)
hist_test2 = cv2.calcHist([hsv_test2], channels, None,
histSize, ranges, accumulate=False)
cv2.normalize(hist_test2, hist_test2, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX)
hist_test3 = cv2.calcHist([hsv_test3], channels, None,
histSize, ranges, accumulate=False)
cv2.normalize(hist_test3, hist_test3, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX)
for compare_method in range(6):
base_base = cv2.compareHist(hist_base, hist_base, compare_method)
base_test1 = cv2.compareHist(hist_base, hist_test1, compare_method)
base_test2 = cv2.compareHist(hist_base, hist_test2, compare_method)
base_test3 = cv2.compareHist(hist_base, hist_test3, compare_method)
print("method[%s]: base_base : %.3f \t base_test1: %.3f \t base_test2: %.3f \t base_test3: %.3f \n" % (
compare_method, base_base, base_test1, base_test2, base_test3))
print("Done \n")
if __name__ == "__main__":
main()
所对比的图片及其在代码中的标识如下所示。其中base图片和test1、test2图片较为相似。test1为base的缩放图,test2是base的水平翻转结果,test3是另外一张完全不同于base的图片。
名字 | 图片 | 标识 |
---|---|---|
lena.jpg | base | |
lena_resize.jpg | test1 | |
lena_flip.jpg | test2 | |
test.jpg | test3 |
识别结果如下,各种方法的评价方式不一样。其中base_base表示base图和base图比较的结果,识别结果大体正确。关于base与test1、test2的对比结果,可以看出来颜色直方图对于图像大小、旋转具有尺寸不变性。
method[0]: base_base : 1.000 base_test1: 0.995 base_test2: 0.998 base_test3: -0.005
method[1]: base_base : 0.000 base_test1: 3.911 base_test2: 0.525 base_test3: 40.661
method[2]: base_base : 40.661 base_test1: 33.850 base_test2: 38.536 base_test3: 0.000
method[3]: base_base : 0.000 base_test1: 0.087 base_test2: 0.046 base_test3: 1.000
method[4]: base_base : 0.000 base_test1: 2.814 base_test2: 0.622 base_test3: 83.835
method[5]: base_base : 0.000 base_test1: 8.674 base_test2: 2.128 base_test3: 864.505
4 反向投影
反向投影(Histogram Backprojection)于1990年在论文Indexing via color histograms提出。反向投影的作用简单来说,就是进行图像分割或在图像中查找感兴趣的对象。通过创建了一个与输入图像大小相同(但只有一个通道)的图像,该图片每个像素对应于该像素属于该兴趣对象的概率。一般步骤为计算某一感兴趣区域特征的直方图模型,然后使用这个直方图模型去寻找图像中和该特征相似的区域。在OpenCV中使用calcBackProject函数来实现反向投影。关于calcBackProject函数介绍见calcBackProject 反向投影。
下面示例展示了反向投影的代码,代码以某块草地图片为感兴趣对象,检索输入图像中包含类似草地的区域。代码中涉及到的fliter2D函数使用见cv.filter2D()函数详解。
C++
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
// 感兴趣区域图片
string roipath = "image/test3.jpg";
// 目标图片
string targetpath = "image/test2.jpg";
Mat target = imread(targetpath);
Mat roi = imread(roipath);
if (target.empty() || roi.empty())
{
cout << "Could not open or find the images!\n" << endl;
return -1;
}
Mat hsv, hsvt;
cvtColor(roi, hsv, COLOR_BGR2HSV);
cvtColor(target, hsvt, COLOR_BGR2HSV);
// 使用前两个通道计算直方图
int channels[] = { 0, 1 };
// 计算颜色直方图
Mat roihist;
int h_bins = 180, s_bins = 256;
int histSize[] = { h_bins, s_bins };
// hue值变化范围为0到179,saturation值变化范围为0到255
float h_ranges[] = { 0, 180 };
float s_ranges[] = { 0, 256 };
const float* ranges[] = { h_ranges, s_ranges };
calcHist(&hsv, 1, channels, Mat(), roihist, 2, histSize, ranges, true, false);
// 归一化图片
normalize(roihist, roihist, 0, 255, NORM_MINMAX, -1, Mat());
// 返回匹配结果图像,dst为一张二值图,白色区域表示匹配到的目标
Mat dst;
calcBackProject(&hsvt, 1, channels, roihist, dst, ranges, 1);
// 应用线性滤波器,理解成去噪就行了
Mat disc = getStructuringElement(MORPH_ELLIPSE, Size(7, 7));
filter2D(dst, dst, -1, disc);
// 阈值过滤
Mat thresh;
threshold(dst, thresh, 50, 255, 0);
// 将thresh转换为3通道图
Mat thresh_group[3] = { thresh, thresh, thresh };
cv::merge(thresh_group, 3, thresh);
imwrite("thresh.jpg", thresh);
// 从图片中提取感兴趣区域
Mat res;
bitwise_and(target, thresh, res);
imshow("target", target);
imshow("thresh", thresh);
imshow("res", res);
waitKey(0);
destroyAllWindows();
return 0;
}
Python
import cv2
def main():
# 感兴趣区域图片
roi = cv2.imread('image/test3.jpg')
# 目标图片
target = cv2.imread('image/test2.jpg')
if roi is None or target is None:
print('Could not open or find the images!')
return -1
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
hsvt = cv2.cvtColor(target, cv2.COLOR_BGR2HSV)
# 计算颜色直方图
roihist = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
# 归一化图片
cv2.normalize(roihist, roihist, 0, 255, cv2.NORM_MINMAX)
# 返回匹配结果图像,dst为一张二值图,白色区域表示匹配到的目标
dst = cv2.calcBackProject([hsvt], [0, 1], roihist, [0, 180, 0, 256], 1)
# 应用线性滤波器,理解成去噪就行了
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
cv2.filter2D(dst, -1, disc, dst)
# 阈值过滤
ret, thresh = cv2.threshold(dst, 50, 255, 0)
# 将thresh转换为3通道图
thresh = cv2.merge((thresh, thresh, thresh))
# 从图片中提取感兴趣区域
res = cv2.bitwise_and(target, thresh)
cv2.imshow("target", target)
cv2.imshow("thresh", thresh)
cv2.imshow("res", res)
cv2.waitKey(0)
cv2.destroyAllWindows()
return 0
if __name__ == "__main__":
main()
结果如下所示,可以看到反向投影结果是不太准的,毕竟是很简单也是很古老的的算法,了解下就好。真正想要实现图像分割,还是看看深度学习。
类型 | 结果 |
---|---|
感兴趣对象 | |
输入对象 | |
反向投影结果 | |
匹配结果 |
5 参考
- 视觉词袋
- OpenCV_Histograms
- 图像直方图均衡化详解
- 限制对比度自适应直方图均衡化算法原理、实现及效果
- YUV图像处理入门1
- OpenCV中的颜色空间
- 基于图像哈希构建图像相似度对比算法
- Indexing via color histograms
- calcBackProject 反向投影
- cv.filter2D() 函数详解