目录
- 3.10 OpenCV中的直方图
- 3.10.1 直方图--1:寻找、绘制、分析
- 目标
- 理论
- 寻找直方图
- 绘制直方图
- 掩膜的应用
- 其他资源
- 3.10.2 直方图--2:直方图均衡化
- 目标
- 理论
- OpenCV中的直方图均衡化
- CLAHE(对比度有限的自适应直方图均衡)
- 其他资源
翻译及二次校对:cvtutorials.com
编辑者:廿瓶鲸(和鲸社区Siby团队成员)
3.10 OpenCV中的直方图
3.10.1 直方图–1:寻找、绘制、分析
目标
学习:
- 使用OpenCV和Numpy函数寻找直方图
- 使用OpenCV和Matplotlib函数绘制直方图
- 你将看到这些函数:cv.calcHist(), np.histogram()等。
理论
那么什么是直方图?你可以把直方图看作是一种图,它可以让你对图像的灰度分布有一个整体的了解。它是一个在X轴上有像素值(范围从0到255,不一定),在Y轴上有图像中相应像素数的图。
它只是理解图像的另一种方式。通过观察图像的直方图,你可以获得关于该图像的对比度、亮度、灰度分布等的直觉。今天,几乎所有的图像处理工具都提供直方图的功能。下面是一张来自Cambridge in Color网站的图片,我建议你访问该网站了解更多细节。
你可以看到这个图像和它的直方图(这个直方图是为灰度图像绘制的,不是彩色图像)。直方图的左边区域显示图像中较暗像素的数量,右边区域显示较亮像素的数量。从直方图中,你可以看到暗色区域比亮色区域多,中间色调(中间范围的像素值,例如127左右)的数量非常少。
寻找直方图
现在我们对什么是直方图有了一个概念,我们可以研究如何找到它。OpenCV和Numpy都有内置的函数来完成这个任务。在使用这些函数之前,我们需要了解一些与直方图有关的术语。
BINS :上面的直方图显示了每个像素值的像素数,即从0到255,即你需要256个值来显示上述直方图。但请考虑,如果你不需要单独找到所有像素值的像素数,而是需要找到像素值的一个区间的像素数,怎么办?例如,你需要找到0到15之间的像素数,然后是16到31,…,240到255。你只需要16个值来表示直方图。
所以你要做的就是简单地把整个直方图分成16个子部分,每个子部分的值是其中所有像素数的总和。这个子部分被称为 “BIN”。在第一种情况下,BIN的数量是256(每个像素一个),而在第二种情况下,它只有16。在OpenCV的文档中,BINS是由术语histSize表示的。
DIMS : 它是我们收集数据的参数数量。在这种情况下,我们只收集一种数据,即灰度值。所以这里是1。
RANGE : 它是你想测量的灰度值的范围。通常情况下,它是[0,256],即所有灰度值。
1.OpenCV中的直方图计算
所以现在我们使用cv.calcHist()函数来寻找直方图。让我们熟悉一下这个函数和它的参数。
cv.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]] )
- images : 它是类型为uint8或float32的源图像,应在方括号内给出,即"[img]"。
- channels : 它也是在方括号中给出的。它是我们计算直方图的通道的索引。例如,如果输入的是灰度图像,它的值是[0]。对于彩色图像,你可以通过[0]、[1]或[2]来分别计算蓝色、绿色或红色通道的直方图。
- mask : 掩膜图像。要查找完整图像的直方图,它被指定为 “None”。但如果你想找到图像特定区域的直方图,你必须为其创建一个掩膜图像并将其作为掩膜(我将在后面展示一个例子)
- histSize:这代表我们的BIN计数。需要在方括号中给出。对于满刻度,我们传递256。
- ranges : 这是我们的RANGE。通常情况下,它是[0,256]。
那么,让我们从一个图像开始。简单地在灰度模式下加载一个图像,然后找到它的直方图。
img = cv.imread('home.jpg',0)
hist = cv.calcHist([img],[0],None,[256],[0,256])
hist是一个256x1的数组,每个值都对应于该图像中的像素数和其对应的像素值。
2.Numpy中的直方图计算
Numpy还为你提供了一个函数,np.histogram()。所以你可以用下面这行来代替calcHist()函数。
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.histogram()快得多(大约10倍)。所以对于一维直方图,你最好试试这个。不要忘记在np.bincount中设置minlength = 256。例如,hist = np.bincount(img.ravel(),minlength=256) OpenCV函数比np.histogram()快(大约40倍)。所以请使用OpenCV函数。
现在我们应该绘制直方图,但如何绘制呢?
绘制直方图
有两种方法可以做到这一点。
- 方法1:使用Matplotlib的绘图函数
- 方法2:使用OpenCV绘图函数
1.使用Matplotlib
Matplotlib有一个直方图绘制函数:matplotlib.pyplot.hist()
它直接找到直方图并绘制出来。你不需要使用calcHist()或np.histogram()函数来寻找直方图。请看下面的代码。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg',0)
plt.hist(img.ravel(),256,[0,256]); plt.show()
你会得到一个如下的图。
或者你可以使用matplotlib的正常绘图模式,这对BGR图很有帮助。为此,你需要先找到直方图的数据。试试下面的代码。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg')
color = ('b', 'g', 'r')
for i,col in enumerate(color):
histr = cv.calcHist([img],[i],None,[256],[0,256])
plt.plot(histr,color = col)
plt.xlim([0,256])
plt.show()
结果:
你可以从上图中推断出,蓝色在图像中有一些高值区域(显然应该是由于天空的原因)。
2.使用OpenCV
我们可以将直方图的值和它的bin值调整成x,y坐标的样子,这样你就可以用cv.line()或cv.polyline()函数来绘制它,生成与上面相同的图像。这在OpenCV-Python2官方样本中已经有了。请查看样本/python/hist.py中的代码。
掩膜的应用
我们用cv.calcHist()来寻找全图的直方图。如果你想找到图像中某些区域的直方图呢?只要在你想找直方图的区域创建一个白色的蒙版图像,其他区域设置为黑色。然后把它作为掩膜传给你。
img = cv.imread('home.jpg',0)
# 创建一个掩膜
mask = np.zeros(img.shape[:2], np.uint8)
mask[100:300, 100:400] = 255
masked_img = cv.bitwise_and(img,img,mask = mask)
# 计算带掩膜和不带掩膜的直方图
# 检查第三个参数的掩膜
hist_full = cv.calcHist([img],[0],None,[256],[0,256] )
hist_mask = cv.calcHist([img],[0],mask,[256],[0,256] )
plt.subplot(221), plt.imshow(img, 'grey')
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()
请看结果。在直方图图中,蓝线显示的是完整图像的直方图,而绿线显示的是被遮蔽区域的直方图。
其他资源
- 彩色剑桥
3.10.2 直方图–2:直方图均衡化
翻译及二次校对:cvtutorials.com
目标
在本节中:
- 我们将学习直方图均衡化的概念,并利用它来改善我们图像的对比度。
理论
考虑一个图像,其像素值只局限于某些特定的数值范围。例如,较亮的图像将有所有的像素限制在高值。但是一个好的图像会有来自图像所有区域的像素。因此,你需要将这个直方图拉伸到两端(如下图所示,来自维基百科),这就是直方图均衡化的作用(简单地说)。这通常会改善图像的对比度。
我建议你阅读关于直方图均衡化的维基百科页面,以了解更多相关细节。它有一个非常好的解释,并有例子,所以在阅读后你会理解几乎所有的东西。相反,在这里我们将看到它的Numpy实现。之后,我们将看到OpenCV函数。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('wiki.jpg',0)
hist,bins = np.histogram(img.flatten(),256,[0,256])
cdf = hist.cumsum()
cdf_normalized = cdf * float(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()
你可以看到直方图位于较亮的区域。我们需要完整的光谱。为此,我们需要一个转换函数,将较亮区域的输入像素映射到完整区域的输出像素。这就是直方图均衡化的作用。
现在我们找到直方图的最小值(不包括0),然后应用wiki页面中给出的直方图均衡化公式。但我在这里使用了Numpy中的掩膜数组概念。对于掩膜数组,所有的操作都是在非掩膜的元素上进行的。你可以从Numpy关于掩膜数组的文档中读到更多关于它的信息。
cdf_m = np.ma.masked_equal(cdf,0)
cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min())
cdf = np.ma.filled(cdf_m,0).astype('uint8')
现在我们有了查找表,它为我们提供了关于每个输入像素值的输出像素值的信息。所以我们只需应用转换。
img2 = cdf[img]
现在我们像以前一样计算它的直方图和Cdf(你来做),结果看起来像下面。
另一个重要的特点是,即使图像是一个较暗的图像(而不是我们使用的一个较亮的图像),在均衡后,我们将得到与上述图像几乎相同的图像。因此,这被用作一个 “参考工具”,使所有图像具有相同的照明条件。这在许多情况下是很有用的。例如,在人脸识别中,在训练人脸数据之前,对人脸图像进行直方图均衡化,使其具有相同的照明条件。
OpenCV中的直方图均衡化
OpenCV有一个函数可以做到这一点,即cv.equalizeHist()。它的输入是灰度图像,输出是我们的直方图均衡化图像。
下面是一个简单的代码片段,显示了它在我们使用的同一图像上的用法。
img = cv.imread('wiki.jpg',0)
equ = cv.equalizeHist(img)
res = np.hstack((img,equ)) #stacking images side-by-side
cv.imwrite('res.png',res)
因此,现在你可以在不同的光线条件下拍摄不同的图像,对其进行均衡并检查结果。
当图像的直方图被限制在一个特定的区域内时,直方图均衡化是好的。在灰度变化较大的地方,即直方图覆盖较大区域的地方,它不会有好的效果,也就是说,明亮和黑暗的像素都存在。请查看附加资源中的SOF链接。
CLAHE(对比度有限的自适应直方图均衡)
我们刚才看到的第一个直方图均衡,考虑了图像的整体对比度。在许多情况下,这并不是一个好主意。例如,下面的图片显示了一张输入图片和全局直方图均衡化后的结果。
诚然,在直方图均衡化之后,背景对比度得到了改善。但比较两张图片中的雕像的脸。由于过亮,我们失去了大部分的信息。这是因为它的直方图并不像我们在以前的案例中看到的那样被限制在一个特定的区域内(试着绘制输入图像的直方图,你会得到更多的直观感受)。
因此,为了解决这个问题,采用了自适应直方图均衡化。在这个过程中,图像被分成小块,称为 “瓦片”(OpenCV中瓦片大小默认为8x8)。然后这些块中的每一个都像往常一样被直方图均衡化。因此,在一个小区域内,直方图将被限制在一个小区域内(除非有噪声)。如果有噪音,它就会被放大。为了避免这种情况,采用了对比度限制。如果任何一个直方图仓超过了指定的对比度限制(在OpenCV中默认为40),在应用直方图均衡化之前,这些像素会被剪掉并均匀地分布到其他仓。在均衡化之后,为了消除瓦片边界的伪影,将应用双线性插值。
下面的代码片段显示了如何在OpenCV中应用CLAHE。
import numpy as np
import cv2 as cv
img = cv.imread('tsukuba_l.png',0)
# create a CLAHE object (Arguments are optional).
clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
cl1 = clahe.apply(img)
cv.imwrite('clahe_2.jpg',cl1)
请看下面的结果,并与上面的结果进行比较,特别是雕像区域。
其他资源
1.维基百科关于直方图均衡化的页面
2.Numpy中的掩膜数组
还可以查看这些关于对比度调整的SOF问题。
1.我如何在OpenCV中用C语言调整对比度?
2.如何用opencv均衡图像的对比度和亮度?