颜色直方图是一种常见的图像特征,顾名思义颜色直方图就是用来反映图像颜色组成分布的直方图。颜色直方图的横轴表示像素值或像素值范围,纵轴表示该像素值范围内像素点的个数或出现频率。颜色直方图属于计算机视觉中的基础概念,其常常被应用于图像相似度计算,视觉词袋,图像颜色平衡等。颜色直方图可以基于不同的颜色空间和坐标系来实现,本文主要基于RGB颜色空间和直角坐标系计算颜色直方图。

颜色直方图是图像的一种全局颜色特征,优点为方法简单、计算迅速、对旋转和尺度等变化不敏感,缺点是忽略了图像的空间分布信息以及用于相似度对比时往往不那么准确。当然对于颜色直方图有一些改进的变种算法,但是本文只介绍最原始的颜色直方图计算方法。因为改进过的算法提效不高,还不如直接用深度学习。本文主要内容有:颜色直方图的计算、图像均衡化、直方图比较和反向投影,涉及到用于直方图计算的OpenCV函数出自OpenCV_Histograms

本文所有代码见:

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实战]52 在OpenCV中使用颜色直方图的更多相关文章

  1. [OpenCV实战]39 在OpenCV中使用ArUco标记的增强现实

    文章目录 1 什么是ArUco标记? 2 在OpenCV中生成ArUco标记 3 检测Aruco标记 4 增强现实应用 5 总结和代码 5.1 生成aruco标记 5.2 使用aruco增强现实 6 ...

  2. [OpenCV实战]50 用OpenCV制作低成本立体相机

    本文主要讲述利用OpenCV制作低成本立体相机以及如何使用OpenCV创建3D视频,准确来说是模仿双目立体相机,我们通常说立体相机一般是指双目立体相机,就是带两个摄像头的那种(目就是指眼睛,双目就是两 ...

  3. [OpenCV实战]48 基于OpenCV实现图像质量评价

    本文主要介绍基于OpenCV contrib中的quality模块实现图像质量评价.图像质量评估Image Quality Analysis简称IQA,主要通过数学度量方法来评价图像质量的好坏. 本文 ...

  4. [OpenCV实战]47 基于OpenCV实现视觉显著性检测

    人类具有一种视觉注意机制,即当面对一个场景时,会选择性地忽略不感兴趣的区域,聚焦于感兴趣的区域.这些感兴趣的区域称为显著性区域.视觉显著性检测(Visual Saliency Detection,VS ...

  5. [OpenCV实战]45 基于OpenCV实现图像哈希算法

    目前有许多算法来衡量两幅图像的相似性,本文主要介绍在工程领域最常用的图像相似性算法评价算法:图像哈希算法(img hash).图像哈希算法通过获取图像的哈希值并比较两幅图像的哈希值的汉明距离来衡量两幅 ...

  6. [OpenCV实战]46 在OpenCV下应用图像强度变换实现图像对比度均衡

    本文主要介绍基于图像强度变换算法来实现图像对比度均衡.通过图像对比度均衡能够抑制图像中的无效信息,使图像转换为更符合计算机或人处理分析的形式,以提高图像的视觉价值和使用价值.本文主要通过OpenCV ...

  7. [OpenCV实战]44 使用OpenCV进行图像超分放大

    图像超分辨率(Image Super Resolution)是指从低分辨率图像或图像序列得到高分辨率图像.图像超分辨率是计算机视觉领域中一个非常重要的研究问题,广泛应用于医学图像分析.生物识别.视频监 ...

  8. [OpenCV实战]36 使用OpenCV在视频中实现简单背景估计

    目录 1 时间中值滤波 2 使用中值进行背景估计 3 帧差分 4 总结和代码 5 参考 许多计算机视觉应用中,硬件配置往往较低.在这种情况下,我们必须使用简单而有效的技术.在这篇文章中,我们将介绍一种 ...

  9. [OpenCV实战]28 基于OpenCV的GUI库cvui

    目录 1 cvui的使用 1.1 如何在您的应用程序中添加cvui 1.2 基本的"hello world"应用程序 2 更高级的应用 3 代码 4 参考 有很多很棒的GUI库,例 ...

随机推荐

  1. 没有使用IaC的DevOps系统都是耍流氓

    作为现代软件工程的基础实践,基础设施即代码(Infrastructure as Code, IaC)是云原生.容器.微服务以及DevOps背后的底层逻辑.应该说,以上所有这些技术或者实践都是以基础设施 ...

  2. 【高并发】ScheduledThreadPoolExecutor与Timer的区别和简单示例

    JDK 1.5开始提供ScheduledThreadPoolExecutor类,ScheduledThreadPoolExecutor类继承ThreadPoolExecutor类重用线程池实现了任务的 ...

  3. 15 Uncaught TypeError: Cannot set properties of null (setting ‘onclick‘)

    1.报错的代码 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <t ...

  4. 微服务开发框架-----Apache Dubbo

    文章目录 一.简介 二.概念与架构 一.简介 Apache Dubbo 是一款微服务开发框架,提供了RPC通信与微服务治理两大关键能力.使用Dubbo开发的微服务,将具备相互之间的远程发现与通信能力, ...

  5. 知识图谱顶会论文(ACL-2022) ACL-SimKGC:基于PLM的简单对比KGC

    12.(2022.5.4)ACL-SimKGC:基于PLM的简单对比KGC 12.(2022.5.4)ACL-SimKGC:基于PLM的简单对比KGC 摘要 1.引言 2.相关工作 2.1 知识图补全 ...

  6. 精简docker的导出镜像

    Docker 镜像是由多个文件系统(只读层)叠加而成,每个层仅包含了前一层的差异部分.当我们启动一个容器的时候,Docker 会加载镜像层并在其上添加一个可写层.容器上所做的任何更改,譬如新建文件.更 ...

  7. 如何在.NET程序崩溃时自动创建Dump?

    今天在浏览张队转载文章的留言时,遇到一个读者问了这样的问题,如下图所示: 首先能明确的一点是"程序崩溃退出了是不能用常规的方式dump的",因为整个进程树都已经退出.现场已经无法使 ...

  8. day08-XML

    XML 官方文档:https://www.w3school.com.cn/xml/index.asp 1.为什么需要xml? 需求1:两个程序间进行数据通信? 需求2:给一台服务器,做一个配置文件,当 ...

  9. VUE3系列---nvm环境搭建

    nvm node version manager:node版本管理工具 可以用来管理多个node版本 1.下载 下载地址:https://github.com/coreybutler/nvm-wind ...

  10. xlwings 模块总结

    基本使用 在子线程中使用时,有时需要在子线程函数中加入以下.有时不需要加入,目前还不明白具体的原因 import pythoncom # 导入的库 pythoncom.CoInitialize() # ...