参考网址:
(1)https://www.cambridgeincolour.com/tutorials/histograms1.htm
(2)https://en.wikipedia.org/wiki/Histogram_equalization
(3)直方图均衡化:https://blog.csdn.net/sunny2038/article/details/9403059
一、直方图的计算,绘制与分析
目标
• 使用 OpenCV 或 Numpy 函数计算直方图
• 使用 Opencv 或者 Matplotlib 函数绘制直方图
• 学习函数: cv2.calcHist(), np.histogram()
原理
什么是直方图呢?通过直方图你可以对整幅图像的灰度分布有一个整体的了解。直方图的 x 轴是灰度值(0 到 255), y 轴是图片中具有同一个灰度值的点的数目。
直方图其实就是对图像的另一种解释。一下图为例,通过直方图我们可以对图像的对比度,亮度,灰度分布等有一个直观的认识。几乎所有的图像处理软件都提供了直方图分析功能。下图来自Cambridge in Color website。

如何获得一副图像的直方图呢?OpenCV 和 Numpy 都有内置函数做这件事,在使用这些函数之前我们有必要想了解一下直方图相关的术语 。
BINS: 上面的直方图显示了每个灰度值对应的像素数。如果像素值为 0到 255,你就需要 256 个数来显示上面的直方图。但是,如果你不需要知道每一个像素值的像素点数目的,而只希望知道两个像素值之间的像素点数目怎么办呢?举例来说,我们想知道像素值在 0 到 15 之间的像素点的数目,接着是 16 到 31,...., 240 到 255。我们只需要 16 个值来绘制直方图。 OpenCVTutirials 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[; accumulate]])
(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]
举个例子:
import cv2
img = cv2.imread('cat.jpg',0)
# 别忘了中括号 [img],[0],None,[256],[0,256],只有 mask 没有中括号
hist = cv2.calcHist([img],[0],None,[256],[0,256])
hist 是一个 256x1 的数组,每一个值代表了与次灰度值对应的像素点数目 。另外,使用 Numpy 统计直方图 Numpy 中的函数 np.histogram() 也可以帮我们统计直方图。你也可以尝试一下下面的代码:
import cv2
import numpy as np
img = cv2.imread('cat.jpg',0)
#img.ravel() 将图像转成一维数组,这里没有中括号。
hist,bins = np.histogram(img.ravel(),256,[0,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.histgram 的 十 倍。 对 于 一 维 直 方 图, 最 好 使 用 这 个函 数。 用 np.bincount 时 别 忘 了 设 置 minlength=256 ,例 如
hist =np.bincount(img.ravel(), minlength=256)
OpenCV
的函数要比 np.histgram() 快 40 倍。所以坚持使用OpenCV 函数。
2、绘制直方图
有两种方法来绘制直方图:
(1)Short Way(简单方法):使用 Matplotlib 中的绘图函数。
(2)Long Way(复杂方法):使用 OpenCV 绘图函数
使用 Matplotlib Matplotlib 中有直方图绘制函数:matplotlib.pyplot.hist()它可以直接统计并绘制直方图。你应该使用函数 calcHist() 或 np.histogram()统计直方图。代码如下:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('cat.jpg',0)
plt.hist(img.ravel(),256,[0,256])
plt.show()
运行结果为:
也可以只使用 matplotlib 的绘图功能, 这在同时绘制多通道(BGR)的直方图,很有用。但是你首先要告诉绘图函数你的直方图数据在哪里。运行一下下面的代码:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('0004.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()
运行结果:
3、使用掩模
要统计图像某个局部区域的直方图只需要构建一副掩模图像。将要统计的部分设置成白色,其余部分为黑色,就构成了一副掩模图像。然后把这个掩模图像传给函数就可以了。
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('0004.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()
运行结果:(其中蓝线是整幅图像的直方图,橙线是进行掩模之后的直方图)
二、直方图均衡化
目标
• 学习直方图均衡化的概念,以及如何使用它来改善图片的对比。
原理
想象一下如果一副图像中的大多是像素点的像素值都集中在一个像素值范围之内会怎样呢?例如,如果一幅图片整体很亮,那所有的像素值应该都会很高。但是一副高质量的图像的像素值分布应该很广泛。所以你应该把它的直方图做一个横向拉伸(如下图),这就是直方图均衡化要做的事情。通常情况下这种操作会改善图像的对比度。
维基百科中关于直方图均衡化的条目的解释得非常给力,读完之后就会对整个过程有一个详细的了解。先看看怎样使用Numpy 来进行直方图均衡化,然后再学习使用 OpenCV 进行直方图均衡化。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('111.png',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()
运行结果:
可以看出来直方图大部分在灰度值较高的部分,而且分布很集中。如果希望直方图的分布比较分散,能够涵盖整个 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]
再根据前面的方法绘制直方图和累积分布图,完整代码如下:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('111.png',0)
#flatten() 将数组变成一维
hist,bins = np.histogram(img.flatten(),256,[0,256])
# 计算累积分布图
cdf = hist.cumsum()
# cdf_normalized = cdf * hist.max()/ cdf.max()
# 构建 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]
plt.subplot(121)
plt.imshow(img2,'gray')
plt.subplot(122)
plt.plot(cdf_m, color = 'b')
plt.hist(img2.flatten(),256,[0,256], color = 'r')
plt.xlim([0,256])
plt.legend(('cdf','histogram'), loc = 'upper left')
plt.show()
结果如下:
即使输入图片是一个比较暗的图片,在经过直方图均衡化之后也能得到相同的结果。因此,直方图均衡化经常用来使所有的图片具有相同的亮度条件的参考工具。这在很多情况下都很有用。例如,脸部识别,在训练分类器前,训练集的所有图片都要先进行直方图均衡化从而使它们达到相同的亮度条件。
1、OpenCV 中的直方图均衡化
import cv2
import numpy as np
img = cv2.imread('111.png',0)
equ = cv2.equalizeHist(img)
res = np.hstack((img,equ))
#stacking images side-by-side
cv2.imwrite('res.png',res)
cv2.namedWindow("Res")
cv2.imshow("Res",res)
cv2.waitKey(0)
运行结果:
