图像直方图 - 1
一、寻找、绘制、分析直方图
在本小节,我们主要进行寻找、绘制(使用matplotlib)、分析直方图信息,用到以下函数: cv.calcHist(), np.histogram() 等等
- 用法如下: cv.calcHist( images, channels, mask, histSize, ranges[, hist[, accumulate]] ) -> hist hist可以直接通过matplot绘制
1. 概念
那么什么是直方图? 您可以将直方图视为图形或绘图,它可以让您全面了解图像的强度分布。 它是 X 轴上的像素值(范围从 0 到 255,并非总是如此)和 Y 轴上图像中相应像素数的图。
这只是理解图像的另一种方式。 通过查看图像的直方图,您可以直观地了解该图像的对比度、亮度、强度分布等。 今天几乎所有的图像处理工具都提供了直方图的特征。 以下是来自 Cambridge in Color 网站的图片,我建议您访问该网站了解更多详细信息。www.cambridgeincolour.com/tutorials/h…
您可以看到图像及其直方图。 (请记住,此直方图是为灰度图像绘制的,而不是彩色图像)。 直方图左侧区域显示图像中较暗像素的数量,右侧区域显示较亮像素的数量。 从直方图可以看出,暗区多于亮区,中间色调(中间范围内的像素值,例如 127 左右)的数量非常少。
2.找出直方图信息
现在我们对什么是直方图有了一个概念,我们可以研究如何找到它。 OpenCV 和 Numpy 都为此提供了内置功能。 在使用这些函数之前,我们需要了解一些与直方图相关的术语。
-
BINS :上面的直方图显示了每个像素值的像素数,即从 0 到 255。即您需要 256 个值来显示上面的直方图。 但是考虑一下,如果您不需要单独找到所有像素值的像素数,而是像素值区间内的像素数,该怎么办? 例如,您需要找到介于 0 到 15、16 到 31、...、240 到 255 之间的像素数。您只需要 16 个值来表示直方图。 这就是 OpenCV 直方图教程中给出的示例中所示的内容。(后面会讲到);因此,您只需将整个直方图拆分为 16 个子部分,每个子部分的值就是其中所有像素数的总和。 这每个子部分称为“BIN”。 在第一种情况下,bin 的数量为 256(每个像素一个),而在第二种情况下,只有 16 个。BINS 在 OpenCV 文档中由术语 histSize 表示。
-
DIMS :这是我们收集数据的参数数量。 在这种情况下,我们只收集关于一件事的数据,即强度值。 所以这里是1。
-
RANGE :这是您要测量的强度值的范围。 通常,它是 [0,256],即所有强度值。
a.OpenCV中的直方图计算
所以现在我们使用 cv.calcHist() 函数来查找直方图。 让我们熟悉一下函数及其参数:cv.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])
参数含义如下:
-
images :它是 uint8 或 float32 类型的源图像。 它应该放在方括号中,即“[img]”。
-
channels:它也在方括号中给出。 它是我们计算直方图的通道索引。 例如,如果输入是灰度图像,则其值为[0]。 对于彩色图像,您可以通过 [0]、[1] 或 [2] 分别计算蓝色、绿色或红色通道的直方图(BGR排列)。
-
mask:掩码图像。 要查找完整图像的直方图,将其指定为“无”。 但是如果你想找到图像特定区域的直方图,你必须为此创建一个蒙版图像并将其作为蒙版。 (稍后我将展示一个示例。)
-
histSize :这表示我们的 BIN 计数。 需要在方括号中给出。 对于全尺寸,我们通过 [256]。
-
ranges:这是我们的强度值范围。 通常,它是 [0,256]。
因此,让我们从示例图像开始。 只需以灰度模式加载图像并找到其完整的直方图。
import cv2 as cv
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
def cv_show(name, img):
cv.imshow(name, img)
cv.waitKey(0)
cv.destroyAllWindows()
def compare(imgs):
# for i in range(len(imgs)):
# imgs[i][:,-3:-1,:] = [255,255,255]
res = np.hstack(imgs)
cv_show('Compare', res)
# 计算灰度图的直方图信息
img = cv.imread('lena.png',0)
# 第一二个参数分别是原图像和通道,都需要用[]括起来
hist = cv.calcHist([img],[0],None,[256],[0,256])
hist 是一个 256x1 数组,每个值对应于该图像中的像素数及其对应的像素值。
# 显示直方图信息
plt.plot(hist)
[<matplotlib.lines.Line2D at 0x20d9bc68a00>]
b.Numpy中的直方图计算
Numpy 还为您提供了一个函数 np.histogram()。 因此,您可以尝试以下代码,来代替 calcHist() 函数:
hist,bins = np.histogram(img.ravel(),256,[0,256])
hist 和我们之前计算的一样。 但是 bin 将有 257 个元素,因为 Numpy 将 bin 计算为 0-0.99、1-1.99、2-2.99 等。所以最终范围是 255-255.99。 为了表示这一点,他们还在bin的末尾添加了 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 功能。
3.绘制直方图
有两种方式进行绘制:
- Short Way:使用 Matplotlib 绘图函数
- Long Way:使用 OpenCV 绘图函数
a.使用Matplotlib
Matplotlib其中有一个绘制函数:matplotlib.pyplot.hist(),传入hist就能进行绘制。
它直接找到直方图并绘制它。 您无需使用 calcHist() 或 np.histogram() 函数来查找直方图。 请看下面的代码:
img = cv.imread('lena.png',0)
plt.subplot('121'),plt.imshow(img, 'gray')
plt.subplot('122'),plt.hist(img.ravel(),256,[0,256])
plt.show()
F:\Temp1/ipykernel_10732/2446630560.py:2: MatplotlibDeprecationWarning: Passing non-integers as three-element position specification is deprecated since 3.3 and will be removed two minor releases later.
plt.subplot('121'),plt.imshow(img, 'gray')
F:\Temp1/ipykernel_10732/2446630560.py:3: MatplotlibDeprecationWarning: Passing non-integers as three-element position specification is deprecated since 3.3 and will be removed two minor releases later.
plt.subplot('122'),plt.hist(img.ravel(),256,[0,256])
或者您可以使用 matplotlib 的普通绘图,这对 BGR 绘图很有用。 为此,您需要先找到直方图数据。 试试下面的代码:
img = cv.imread('lena.png')
color = ('Blue','Green','Red')
for i,col in enumerate(color):
# 得到每一个通道的直方图信息
histr = cv.calcHist([img],[i],None,[256],[0,256])
plt.plot(histr,color = col,label = color[i])
# 设定 x 轴的限度
plt.xlim([0,256])
plt.legend()
plt.show()
从上图中可以推断,红色在图像中有一些高值区域(显然应该是整体色调的原因)
b.使用OpenCV
好吧,在这里您调整直方图的值及其 bin 值,使其看起来像 x,y 坐标,以便您可以使用 cv.line() 或 cv.polyline() 函数绘制它以生成与上面相同的图像。 这已经在 OpenCV-Python2 官方示例中可用。 检查示例/python/hist.py 中的代码。
4.使用掩码
我们使用 cv.calcHist() 来查找完整图像的直方图。 如果你想找到图像某些区域的直方图怎么办? 只需在要查找直方图的区域上创建一个白色的蒙版图像,否则为黑色。 然后将其作为掩码传递。
img = cv.imread('lena.png',0)
mask = np.zeros((512,512), np.uint8)
mask[128:255,128:255] = 255
res = cv.bitwise_and(img, img, mask = mask)
hist_img = cv.calcHist([img], [0],None, [256], [0, 256])
hist_mask = cv.calcHist([img], [0],mask, [256], [0, 256])
compare([img, mask, res])
plt.plot(hist_img, label = 'Origin')
plt.plot(hist_mask, label= 'Mask')
plt.legend()
plt.show()
# img = cv.imread('lena.png',0)
# # create a mask
# mask = np.zeros(img.shape[:2], np.uint8)
# mask[100:300, 100:400] = 255
# masked_img = cv.bitwise_and(img,img,mask = mask)
# # Calculate histogram with mask and without mask
# # Check third argument for 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, '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()
二、直方图均衡
本小节主要介绍直方图均衡的概念并利用其来增强图像的对比度。
1. 概念
考虑一个图像,其像素值仅限于某个特定的值范围。 例如,较亮的图像将所有像素限制为高值。 但是好的图像将具有来自图像所有区域的像素。 因此,您需要将此直方图拉伸到任一端(如下图所示,来自维基百科),这就是直方图均衡所做的(简单来说)。 这通常会提高图像的对比度。
我建议您阅读有关直方图均衡的维基百科页面以获取更多详细信息en.wikipedia.org/wiki/Histog… 它有一个很好的解释和制定的例子,这样你在阅读之后就会理解几乎所有的东西。 另外,在这里我们将看到它的 Numpy 实现。 之后,我们将看到 OpenCV 函数。
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.subplot(121),plt.imshow(img,'gray')
plt.subplot(122),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 docs on masked arrays 中了解更多信息。
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]
hist,bins = np.histogram(img2.flatten(),256,[0,256])
cdf = hist.cumsum()
cdf_normalized = cdf * float(hist.max()) / cdf.max()
plt.subplot(121),plt.imshow(img2,'gray')
plt.subplot(122),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()
另一个重要的特点是,即使图像是较暗的图像(而不是我们使用的较亮的图像),在均衡之后我们将获得几乎与我们得到的图像相同的图像。 因此,这被用作“参考工具”,以使所有图像具有相同的照明条件。 这在很多情况下都很有用。 例如,在人脸识别中,在训练人脸数据之前,对人脸图像进行直方图均衡,使它们都具有相同的光照条件。
2. 使用OpenCV进行直方图均衡
OpenCV 有一个函数可以做到这一点,cv.equalizeHist()。 它的输入只是灰度图像,输出是我们的直方图均衡图像。 下面是一个简单的代码片段,显示了我们使用的同一图像的用法:
- 用法如下: cv.equalizeHist( src[, dst] ) -> dst
img = cv.imread('wiki.jpg',0)
img_eq = cv.equalizeHist(img)
hist = cv.calcHist(img, [0], None, [255], [0, 256])
hist_eq = cv.calcHist(img_eq, [0], None, [255], [0, 256])
compare([img, img_eq])
plt.plot(hist,label = 'Origin')
plt.plot(hist_eq, label = 'Equalization')
plt.legend()
plt.show()
有一点去雾的效果了,2333
所以现在你可以在不同的光照条件下拍摄不同的图像,均衡它并检查结果。 当图像的直方图被限制在特定区域时,直方图均衡化效果很好。 在直方图覆盖大区域(即同时存在亮像素和暗像素)的地方,它不会很好地工作。 请查看附加资源中的 SOF 链接。
3. CLAHE 对比定限自适应直方图均衡
我们刚刚看到的第一个直方图均衡化考虑了图像的全局对比度。 在许多情况下,这不是一个好主意。 例如,下图显示了全局直方图均衡后的输入图像及其结果。
确实,在直方图均衡后背景对比度有所改善。 但是比较两个图像中雕像的脸。 由于过亮,我们在那里丢失了大部分信息。 这是因为它的直方图并不像我们在前面的例子中看到的那样局限于特定区域(尝试绘制输入图像的直方图,你会得到更多的直观感受)。
所以为了解决这个问题,使用了自适应直方图均衡。 在这种情况下,图像被分成称为“tiles”的小块(在 OpenCV 中,tileSize 默认为 8x8)。 然后像往常一样对这些块中的每一个进行直方图均衡。 所以在一个小区域内,直方图会限制在一个小区域内(除非有噪音)。 如果有噪音,它会被放大。 为了避免这种情况,应用了对比度限制。 如果任何直方图 bin 高于指定的对比度限制(OpenCV 中默认为 40),则在应用直方图均衡之前,这些像素将被裁剪并均匀分布到其他 bin。 在均衡之后,为了去除tiles边界中的伪影,应用了双线性插值。
我们要先创建一个ClAHE对象,再使用其的apply方法进行自适应滤波
- cv.createCLAHE( [, clipLimit[, tileGridSize]] ) -> retval
img = cv.imread('R.png',0)
# create a CLAHE object (Arguments are optional).
# 第一个参数是对比度限制,第二个参数是(8, 8)进行直方图均衡的单位
clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
cl1 = clahe.apply(img)
compare([img, cl1])
可以看到,雕像的面部细节保留的同时,书架背景的对比度也提升了!
三、补充知识
- Wikipedia page on Histogram Equalization:en.wikipedia.org/wiki/Histog…
- Masked Arrays in Numpy:docs.scipy.org/doc/numpy/r…
- How can I adjust contrast in OpenCV in C?stackoverflow.com/questions/1…
- How do I equalize contrast & brightness of images using opencv?stackoverflow.com/questions/1…