OpenCV-Python学习笔记(十三):直方图的计算绘制与分析、直方图均衡化、2D直方图、直方图反向投影

1 直方图的计算,绘制与分析


目标:
• 使用 OpenCV 或 Numpy 函数计算直方图
• 使用 Opencv 或者 Matplotlib 函数绘制直方图
• 将要学习的函数有: cv2.calcHist( )np.histogram( )
 

通过直方图你可以对整幅图像的灰度分布有一个整体的了解。直方图的 x 轴是灰度值(0 到 255), y 轴是图片中具有同一个灰度值的点的数目。直方图其实就是对图像的另一种解释。
 

 1.1 统计直方图

 先了解一下直方图相关的术语。

BINS:一般的直方图显示了每个灰度值对应的像素数。如果像素值为 0到 255,你就需要 256 个数来显示上面的直方图。但是,如果你不需要知道每一个像素值的像素点数目的,而只希望知道两个像素值之间的像素点数目怎么办呢?举例来说,我们想知道像素值在 0 到 15 之间的像素点的数目,接着是 16 到 31,...., 240 到 255。我们只需要 16 个值来绘制直方图。可以查阅 OpenCV Tutorials on histograms中例子所演示的内容。

那到底怎么做呢?你只需要把原来的 256 个值等分成 16 小组,取每组的总和。而这里的每一个小组就被称为 BIN。第一个例子中有 256 个 BIN,第二个例子中有 16 个 BIN。在 OpenCV 的文档中用 histSize 表示 BINS。

DIMS:表示我们收集数据的参数数目。在本例中,我们对收集到的数据只考虑一件事:灰度值。所以这里就是 1。
RANGE:就是要统计的灰度值范围,一般来说为 [0, 256]。

使用 OpenCV 统计直方图:函数 cv2.calcHist()可以帮助我们统计一幅图像的直方图。我们一起来熟悉一下这个函数和它的参数:cv2.calcHist(images, channels, mask, histSize, ranges, hist=None, accumulate=None)

1. images: 原图像(图像格式为 uint8 或 float32)。当传入函数时应该用中括号 [ ] 括起来,例如: [img]。
2. channels: 同样需要用中括号括起来,表示图像通道的索引,传入不同的索引值就会统计该索引对应通道的直方图。如果输入图像是灰度图,它的值就是 [0];如果是彩色图像,传入的参数可以是 [0], [1], [2] 它们分别对应着通道 B, G, R。
3. mask: 掩模图像。要统计整幅图像的直方图就把它设为 None。但是如果你想统计图像某一部分的直方图的话,你就需要制作一个掩模图像,并使用它。(后边有例子)
4. histSize: BIN 的数目。也应该用中括号括起来,例如: [256]。
5. ranges: 像素值范围,通常为 [0, 256]

让我们从一副简单图像开始吧。以灰度格式加载一幅图像并统计图像的直方图。

img = cv2.imread('home.jpg', 0)
# 别忘了中括号 [img],[0],None,[256],[0,256],只有 mask 没有中括号
hist = cv2.calcHist([img], [0], None, [256], [0,256])

hist 是一个 256x1 的数组,每一个值代表了与次灰度值对应的像素点数目。

使用 Numpy 统计直方图:Numpy 中的函数 np.histogram( ) 也可以帮我们统计直方图。你也可以尝试一下下面的代码:

# 下面的 img.ravel() 将图像转成一维数组,这里没有中括号
hist, bins = np.histogram(img.ravel(), 256, [0,256])

# np.bincount()函数比np.histogram()快十倍,一维直方图最好用这个函数,如下:
hist=np.bincount(img.ravel(), minlength=256)

hist 与上面计算的一样。但是这里的 bins 是 257,因为 Numpy 计算bins 的方式为: 0-0.99,1-1.99,2-2.99 等。所以最后一个范围是 255-255.99。为了表示它,所以在 bins 的结尾加上了 256。但是我们不需要 256,到 255就够了。

其 他: Numpy 还 有 一 个 函 数 np.bincount( ), 它 的 运 行 速 度 是np.histogram( ) 的 十 倍。 所 以 对 于 一 维 直 方 图, 我 们 最 好 使 用 这 个函 数。 使 用 np.bincount 时 别 忘 了 设 置 minlength=256。 例 如,

hist=np.bincount(img.ravel(), minlength=256)

注意: OpenCV 的函数要比 np.histgram() 快 40 倍。所以坚持使用OpenCV 函数更好。  

1.2 绘制直方图 

 有两种方法来绘制直方图:

1. Short Way(简单方法):使用 Matplotlib 中的绘图函数:matplotlib.pyplot.hist( )
2. Long Way(复杂方法):使用 OpenCV 绘图函数

使用 Matplotlib:Matplotlib 中有直方图绘制函数:matplotlib.pyplot.hist( )它可以直接统计并绘制直方图。不需要使用函数 cv2.calcHist() 或 np.histogram()来统计直方图。代码如下:

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('home.jpg',0)
plt.hist(img.ravel(), 256, [0,256]);
plt.show()

结果图:

histogram_matplotlib.jpg

或者你可以只使用 matplotlib 的绘图功能,这在同时绘制多通道(BGR)的直方图,很有用。但是你首先要告诉绘图函数你的直方图数据在哪里。如下面的代码:

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('home.jpg')
color = ('b', 'g', 'r')
# 对一个列表或数组既要遍历索引又要遍历元素时
# 使用内置 enumerrate 函数会有更加直接,优美的做法
# enumerate 会将数组或列表组成一个索引序列。
# 使我们再获取索引和索引内容的时候更加方便
for i, col in enumerate(color):
    histr = cv2.calcHist([img], [i], None, [256], [0, 256])
    plt.plot(histr, color=col)
    plt.xlim([0, 256])
plt.show()

结果:

histogram_rgb_plot.jpg

从上边的直方图你可以推断出蓝色曲线靠右侧的最多(很明显这些就是天空)

使用 OpenCV : 使用 OpenCV 自带函数绘制直方图比较麻烦,这里不作介绍,有兴趣可以自己研究。可以参考 OpenCV-Python2 的官方示例。在这里你可以调整直方图的值及其bin值,使其看起来像x,y坐标,这样你就可以使用cv.line()或cv.polyline()函数绘制它,以生成与上面相同的图像。这已经在OpenCV-Python2官方样本中提供。检查samples / python / hist.py上的代码。

 

1.3 使用掩模

要统计图像某个局部区域的直方图只需要构建一副掩模图像。将要统计的部分设置成白色,其余部分为黑色,就构成了一副掩模图像。然后把这个掩模图像传给函数就可以了。

img = cv2.imread('home.jpg', 0)

# create a mask
mask = np.zeros(img.shape[:2], np.uint8)
mask[100:300, 100:400] = 255
masked_img = cv2.bitwise_and(img, img, mask=mask)

# Calculate histogram with mask and without mask
# Check third argument for mask
hist_full = cv2.calcHist([img], [0], None, [256], [0, 256])
hist_mask = cv2.calcHist([img], [0], mask, [256], [0, 256])

plt.subplot(221), plt.imshow(img, 'gray')
plt.subplot(222), plt.imshow(mask, 'gray')
plt.subplot(223), plt.imshow(masked_img, 'gray')
plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask)
plt.xlim([0, 256])

plt.show()

 结果如下,其中蓝线是整幅图像的直方图,绿线是进行掩模之后的直方图。

histogram_masking.jpg

 2 直方图均衡化(Histogram Equalization)

 目标:本小节我们要学习直方图均衡化的概念,以及如何使用它来改善图片的对比度。

先看看怎样使用Numpy 来进行直方图均衡化,然后再学习使用 OpenCV 进行直方图均衡化。

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('wiki.jpg', 0)

# flatten() 将数组变成一维
hist, bins = np.histogram(img.flatten(), 256, [0, 256])
# 计算累积分布图
cdf = hist.cumsum()
cdf_normalized = cdf * hist.max() / cdf.max()

plt.plot(cdf_normalized, color='b')
plt.hist(img.flatten(), 256, [0, 256], color='r')
plt.xlim([0, 256])
plt.legend(('cdf', 'histogram'), loc='upper left')
plt.show()

histeq_numpy1.jpg

我们可以看出来直方图大部分在灰度值较高的部分,而且分布很集中。而我们希望直方图的分布比较分散,能够涵盖整个 x 轴。所以,我们就需要一个变换函数帮助我们把现在的直方图映射到一个广泛分布的直方图中。这就是直方图均衡化要做的事情。


我们现在要找到直方图中的最小值(除了 0),并把它用于 wiki 中的直方图均衡化公式。但是我在这里使用了 Numpy 的掩模数组。对于掩模数组的所有操作都只对 non-masked 元素有效。你可以到 Numpy 文档中获取更多掩模数组的信息。

# 构建 Numpy 掩模数组, cdf 为原数组,当数组元素为 0 时,掩盖(计算时被忽略)。
cdf_m = np.ma.masked_equal(cdf,0)
cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min())
# 对被掩盖的元素赋值,这里赋值为 0
cdf = np.ma.filled(cdf_m,0).astype('uint8')

现在就获得了一个表,我们可以通过查表得知与输入像素对应的输出像素的值。我们只需要把这种变换应用到图像上就可以了。

img2 = cdf[img]

 我们再根据前面的方法绘制直方图和累积分布图,结果如下:

histeq_numpy2.jpg

直方图均衡化经常用来使所有的图片具有相同的亮度条件的参考工具。这在很多情况下都很有用。例如,脸部识别,在训练分类器前,训练集的所有图片都要先进行直方图均衡化从而使它们达到相同的亮度条件。

2.1 OpenCV 中的直方图均衡化

OpenCV 中的直方图均衡化函数为 cv2.equalizeHist()。这个函数的输入图片是一副灰度图像,输出结果是直方图均衡化之后的图像。
下边的代码还是对上边的那幅图像进行直方图均衡化:

img = cv2.imread('wiki.jpg',0)
equ = cv2.equalizeHist(img)
res = np.hstack((img,equ))
#stacking images side-by-side
cv2.imwrite('res.png',res)

equalization_opencv.jpg

2.2 CLAHE 有限对比适应性直方图均衡化

我们在上边做的直方图均衡化会改变整个图像的对比度,但是在很多情况下,这样做的效果并不好。例如,下图分别是输入图像和进行直方图均衡化之后的输出图像。

的确在进行完直方图均衡化之后,图片背景的对比度被改变了。但是你再对比一下两幅图像中雕像的面图,由于太亮我们丢失了很多信息。造成这种结果的根本原因在于这幅图像的直方图并不是集中在某一个区域(试着画出它的直方图,你就明白了)。


为了解决这个问题,我们需要使用自适应的直方图均衡化。这种情况下,整幅图像会被分成很多小块,这些小块被称为“tiles"(在 OpenCV 中 tiles 的大小默认是 8x8),然后再对每一个小块分别进行直方图均衡化(跟前面类似)。所以在每一个的区域中,直方图会集中在某一个小的区域中(除非有噪声干扰)。如果有噪声的话,噪声会被放大。为了避免这种情况的出现要使用对比度限制。对于每个小块来说,如果直方图中的 bin 超过对比度的上限的话,就把其中的像素点均匀分散到其他 bins 中,然后在进行直方图均衡化。最后,为了去除每一个小块之间“人造的”(由于算法造成)边界,再使用双线性差值,对小块进行缝合。

下面的代码显示了如何使用 OpenCV 中的 CLAHE。

import numpy as np
import cv2

img = cv2.imread('tsukuba_l.png',0)

# create a CLAHE object (Arguments are optional).
# 不知道为什么我没好到 createCLAHE 这个模块
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
cl1 = clahe.apply(img)

cv2.imwrite('clahe_2.jpg',cl1)

下面就是结果了,与前面的结果对比一下,尤其是雕像区域:

clahe_2.jpg

更多资源:
1. 维基百科中的直方图均衡化
2. Masked Arrays in Numpy
关于调整图片对比度 SOF 问题:
1. 在 C 语言中怎样使用 OpenCV 调整图像对比度?
2. 怎样使用 OpenCV 调整图像的对比度和亮度?

 

3  2D 直方图

3.1 介绍
在前面的部分我们介绍了如何绘制一维直方图,之所以称为一维,是因为我们只考虑了图像的一个特征:灰度值。但是在 2D 直方图中我们就要考虑两个图像特征。对于彩色图像的直方图通常情况下我们需要考虑每个的颜色(Hue)和饱和(Saturation)。根据这两个特征绘制 2D 直方图。在(samples/python/color_histogram.py) 中包含一个创建彩色直方图的例子。本节就学习如何绘制颜色直方图,这会对我们下一节学习直方图投影有所帮助。

3.2 OpenCV 中的 2D 直方图

使用函数 cv2.calcHist() 来计算直方图既简单又方便。如果要绘制颜色直方图的话,我们首先需要将图像的颜色空间从 BGR 转换到 HSV。(记住,计算一维直方图,要从 BGR 转换到 HSV)。计算 2D 直方图,函数的参数要做如下修改:
• channels=[0, 1] 因为我们需要同时处理 H 和 S 两个通道。
• bins=[180, 256] H 通道为 180, S 通道为 256。
• range=[0, 180, 0, 256] H 的取值范围在 0 到 180, S 的取值范围在 0 到 256。

代码如下:

import cv2
import numpy as np
img = cv2.imread('home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])

3.3 Numpy 中 2D 直方图
Numpy 同样提供了绘制 2D 直方图的函数: np.histogram2d()。(还记得吗,绘制 1D 直方图时我们使用的是 np.histogram())

import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist, xbins, ybins = np.histogram2d(h.ravel(),s.ravel(),[180,256],[[0,180],[0,256]])

第一个参数是 H 通道,第二个参数是 S 通道,第三个参数是 bins 的数目,第四个参数是数值范围。现在我们要看看如何绘制颜色直方图。

3.4 绘制 2D 直方图
方法 1:使用 cv2.imshow() 我们得到结果是一个 180x256 的两维数组。所以我们可以使用函数 cv2.imshow() 来显示它。但是这是一个灰度图,除非我们知道不同颜色 H 通道的值,否则我们根本就不知道那到底代表什么颜色。

方法 2:使用 matplotlib.pyplot.imshow() 我们还可以使用函数 matplotlib.pyplot.imshow()来绘制 2D 直方图,再搭配上不同的颜色图(color_map)。这样我们会对每个点所代表的数值大小有一个更直观的认识。但是跟前面的问题一样,你还是不知道那个数代表的颜色到底是什么。虽然如此,我还是更喜欢这个方法,它既简单又好用。

注意: 在使用这个函数时,要记住设置插值参数为 nearest。

代码如下:

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist = cv2.calcHist( [hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )

plt.imshow(hist,interpolation = 'nearest')
plt.show()

 下面是输入图像和颜色直方图。 X 轴显示 S 值, Y 轴显示 H 值。

2dhist_matplotlib.jpg

在直方图中,你可以看到在 H=100, S=100 附近有比较高的值。这部分与天的蓝色相对应。同样另一个峰值在 H=25 和 S=100 附近。这一宫殿的黄色相对应。你可用通过使用图像编辑软件(GIMP)修改图像,然后在绘制直方图看看我说的对不对。
 

方法 3: OpenCV 风格 在samples (samples/python/color_histogram.py)文档中有一个关于颜色直方图的例子。运行一下这个代码,你看到的颜色直方图也显示了对应的颜色。简单来说就是:输出结果是一副由颜色编码的直方图。效果非常好(虽然要添加很多代码)。在那个代码中,作者首先创建了一个 HSV 格式的颜色地图,然后把它转换成 BGR 格式。再将得到的直方图与颜色直方图相乘。作者还用了几步来去除小的孤立的的点,从而得到了一个好的直方图。
下边图片是对上面图片运行这段代码之后得到的结果:

2dhist_opencv.jpg

从直方图中我们可以很清楚的看出它们代表的颜色,蓝色,换色,还有棋盘带来的白色,漂亮!!!

 

4 直方图反向投影

直方图反向投影是由 Michael J. Swain 和 Dana H. Ballard 在他们的文章“Indexing via color histograms”中提出。

直方图反向投影可以用来做图像分割,或者在图像中找寻我们感兴趣的部分。简单来说,它会输出与输入图像(待搜索)同样大小的图像,其中的每一个像素值代表了输入图像上对应点属于目标对象的概率。用更简单的话来解释,输出图像中像素值越高(越白)的点就越可能代表我们要搜索的目标(在输入图像所在的位置)。这是一个直观的解释。直方图投影经常与 camshift算法等一起使用。

我们应该怎样来实现这个算法呢?首先我们要为一张包含我们要查找目标的图像创建直方图(在我们的示例中,我们要查找的是草地,其他的都不要)。我们要查找的对象要尽量占满这张图像(换句话说,这张图像上最好是有且仅有我们要查找的对象)。最好使用颜色直方图,因为一个物体的颜色要比它的灰度能更好的被用来进行图像分割与对象识别。接着我们再把这个颜色直方图投影到输入图像中寻找我们的目标,也就是找到输入图像中的每一个像素点的像素值在直方图中对应的概率,这样我们就得到一个概率图像,最后设置适当的阈值对概率图像进行二值化,就这么简单。


4.1 Numpy 中的算法 

此处的算法与上边介绍的算法稍有不同。首先,我们要创建两幅颜色直方图,目标图像的直方图('M'),(待搜索)输入图像的直方图('I')。

import cv2
import numpy as np
from matplotlib import pyplot as plt

#roi is the object or region of object we need to find
roi = cv2.imread('rose_red.png')
hsv = cv2.cvtColor(roi,cv2.COLOR_BGR2HSV)

#target is the image we search in
target = cv2.imread('rose.png')
hsvt = cv2.cvtColor(target,cv2.COLOR_BGR2HSV)

# Find the histograms using calcHist. Can be done with np.histogram2d also
M = cv2.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )
I = cv2.calcHist([hsvt],[0, 1], None, [180, 256], [0, 180, 0, 256] )

计算比值:R = \frac{M}{I} 。反向投影 R,也就是根据 R 这个”调色板“创建一副新的图像,其中的每一个像素代表这个点就是目标的概率。例如 B (x,y) = R [h (x,y) ,s (x, y)],其中 h 为点(x, y)处的 hue 值, s 为点(x, y)处的saturation 值。最后再加入一个条件 B (x, y) = min [B (x, y) , 1]。

h,s,v = cv2.split(hsvt)
B = R[h.ravel(), s.ravel()]
B = np.minimum(B, 1)
B = B.reshape(hsvt.shape[:2])

 现在使用一个圆盘算子做卷积, B = D × B,其中 D 为卷积核。

disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
B=cv2.filter2D(B, -1, disc)
B = np.uint8(B)
cv2.normalize(B, B, 0, 255, cv2.NORM_MINMAX)

现在输出图像中灰度值最大的地方就是我们要查找的目标的位置了。如果我们要找的是一个区域,我们就可以使用一个阈值对图像进行二值化,这样就可以得到一个很好的结果了。

ret,thresh = cv2.threshold(B, 50, 255, 0)

 4.2 OpenCV 中的反向投影

OpenCV 提供的函数 cv2.calcBackProject( ) 可以用来做直方图反向投影。它的参数与函数 cv2.calcHist 的参数基本相同。其中的一个参数是我们要查找目标的直方图。同样在使用目标的直方图做反向投影之前我们应该先对其做归一化处理。返回的结果是一个概率图像,我们再使用一个圆盘形卷积核对其做卷操作,最后使用阈值进行二值化。下面就是代码和结果:

import cv2
import numpy as np

roi = cv2.imread('tar.jpg')
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)

target = cv2.imread('roi.jpg')
hsvt = cv2.cvtColor(target, cv2.COLOR_BGR2HSV)

# calculating object histogram
roihist = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])

# normalize histogram and apply backprojection
# 归一化:原始图像,结果图像,映射到结果图像中的最小值,最大值,归一化类型
# cv2.NORM_MINMAX 对数组的所有值进行转化,使它们线性映射到最小值和最大值之间
# 归一化之后的直方图便于显示,归一化之后就成了 0 到 255 之间的数了。
cv2.normalize(roihist, roihist, 0, 255, cv2.NORM_MINMAX)
dst = cv2.calcBackProject([hsvt], [0, 1], roihist, [0, 180, 0, 256], 1)

# Now convolute with circular disc
# 此处卷积可以把分散的点连在一起
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
dst = cv2.filter2D(dst, -1, disc)

# threshold and binary AND
ret, thresh = cv2.threshold(dst, 50, 255, 0)
# 别忘了是三通道图像,因此这里使用 merge 变成 3 通道
thresh = cv2.merge((thresh, thresh, thresh))
# 按位操作
res = cv2.bitwise_and(target, thresh)

res = np.hstack((target, thresh, res))
cv2.imwrite('res.jpg', res)
# 显示图像
cv2.imshow('1', res)
cv2.waitKey(0)

下面是我使用的一幅图像。我使用图中蓝色矩形中的区域作为取样对象,再根据这个样本搜索图中所有的类似区域(草地)。

backproject_opencv.jpg

 更多资源
1. “Indexing via color histograms”, Swain, Michael J. , Third international conference on computer vision,1990.

 

参考资料:

1.  OpenCV-Python官方文档

2. 《OpenCV-Python 中文教程》(段力辉 译)

3. Histogram Calculation

 

 

 

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值