使用OpenCV和Numpy函数查找直方图,使用OpenCV和Matplotlib函数绘制直方图
函数: cv.calcHist(), np.histogram()等
通过观察图像的直方图,你可以直观地了解图像的对比度、亮度、强度分布等。现在几乎所有的图像处理工具都提供了直方图的功能。下面是来自 Cambridge in Color website的一张图片。
上面为图像及其直方图。(请记住,此直方图是为灰度图像绘制的,而不是彩色图像)。直方图左侧区域表示图像中较暗像素的数量,右侧区域表示较亮像素的数量。从直方图中,可以看到暗区域比亮区域多,中间色调的数量(像素值在中间范围,比如127)非常少。
现在我们知道什么是直方图了,我们可以看看如何找到它。OpenCV和Numpy都内置了这个函数。在使用这些函数之前,我们需要了解一些与直方图相关的术语。
BINS :
上面的直方图显示了每个像素值的像素数,即从0到255。需要256个值来显示上面的直方图。但是考虑一下,如果你不需要分别找到所有像素值的像素数,而是需要找到像素值区间内的像素数,那该怎么办?例如,你需要找到0到15之间的像素数,然后是16到31,…, 240 ~ 255。你将只需要16个值来表示直方图。这就是文中给出的例子 OpenCV Tutorials on histograms.
所以你要做的就是简单地把整个直方图分成16个子部分,每个子部分的值是其中所有像素数的总和。每个子部分称为“BIN”。在第一种情况下,bin的数量是256(每个像素一个),而在第二种情况下,它只有16。OpenCV docs中以术语histSize表示BINS。
DIMS:它是我们采集数据的参数数量(其实就是统计量的维度吧)。在这种情况下,我们只收集亮度强度值这一个数据。所以维度为1。
RANGE :这是你想测量的强度值的范围。通常,它是[0,256],即所有强度值。
现在我们使用 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]。
RANGE:通常是[0,256]。
让我们从一个图像例子开始。以灰度模式加载图像,并找到其完整的直方图。
img = cv.imread('home.jpg',0)
hist = cv.calcHist([img],[0],None,[256],[0,256])
Hist是一个256x1数组,每个值对应于该图像中像素的数量及其对应的像素值。
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()快得多(大约10X)。对于一维直方图,你可以尝试一下。不要忘记在np.bincount中设置minlength = 256。例如,hist = np.bincount(img.ravel(),minlength=256)
OpenCV函数比np.histogram()快(大约40X)。所以坚持使用OpenCV函数。
有两种方法,
简单方法:使用Matplotlib绘图函数
稍微麻烦的方法:使用OpenCV绘图函数
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()
在这里,你可以调整直方图的值以及它的bin值,使其看起来像x、y坐标,这样你就可以使用cv.line()或cv.polyline()函数来绘制它,从而生成与上面相同的图像。这已经在OpenCV-Python2官方示例中可用。查看samples/python/hist.py中的代码。
#!/usr/bin/env python# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
bins = np.arange(256).reshape(256,1)
def hist_curve(im):
h = np.zeros((300,256,3))
if len(im.shape) == 2:
color = [(255,255,255)]
elif im.shape[2] == 3:
color = [ (255,0,0),(0,255,0),(0,0,255) ]
for ch, col in enumerate(color):
hist_item = cv.calcHist([im],[ch],None,[256],[0,256])
cv.normalize(hist_item,hist_item,0,255,cv.NORM_MINMAX)
hist=np.int32(np.around(hist_item))
pts = np.int32(np.column_stack((bins,hist)))
cv.polylines(h,[pts],False,col)
y=np.flipud(h)
return y
def hist_lines(im):
h = np.zeros((300,256,3))
if len(im.shape)!=2:
print("hist_lines applicable only for grayscale images")
#print("so converting image to grayscale for representation"
im = cv.cvtColor(im,cv.COLOR_BGR2GRAY)
hist_item = cv.calcHist([im],[0],None,[256],[0,256])
cv.normalize(hist_item,hist_item,0,255,cv.NORM_MINMAX)
hist=np.int32(np.around(hist_item))
for x,y in enumerate(hist):
cv.line(h,(x,0),(x,y),(255,255,255))
y = np.flipud(h)
return y
def main():
import sys
im = cv.imread('../data/flange/test7.bmp')
if im is None:
print('Failed to load image……')
sys.exit(1)
gray = cv.cvtColor(im,cv.COLOR_BGR2GRAY)
print(''' Histogram plotting \n
Keymap :\n
a - show histogram for color image in curve mode \n
b - show histogram in bin mode \n
c - show equalized histogram (always in bin mode) \n
d - show histogram for color image in curve mode \n
e - show histogram for a normalized image in curve mode \n
Esc - exit \n
''')
cv.imshow('image',im)
while True:
k = cv.waitKey(0)
if k == ord('a'):
curve = hist_curve(im)
cv.imshow('histogram',curve)
cv.imshow('image',im)
print('a')
elif k == ord('b'):
print('b')
lines = hist_lines(im)
cv.imshow('histogram',lines)
cv.imshow('image',gray)
elif k == ord('c'):
print('c')
equ = cv.equalizeHist(gray)
lines = hist_lines(equ)
cv.imshow('histogram',lines)
cv.imshow('image',equ)
elif k == ord('d'):
print('d')
curve = hist_curve(gray)
cv.imshow('histogram',curve)
cv.imshow('image',gray)
elif k == ord('e'):
print('e')
norm = cv.normalize(gray, gray, alpha = 0,beta = 255,norm_type = cv.NORM_MINMAX)
lines = hist_lines(norm)
cv.imshow('histogram',lines)
cv.imshow('image',norm)
elif k == 27:
print('ESC')
cv.destroyAllWindows()
break
print('Done')
if __name__ == '__main__':
print(__doc__)
main()
cv.destroyAllWindows()
我们使用cv.calcHist()来查找完整图像的直方图。如果你想找到图像某些区域的直方图呢?只需在你想要找到直方图的区域上创建一个掩膜,并传递到函数里面。
img = cv.imread('home.jpg',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()
看到结果。在直方图中,蓝线表示全图像的直方图,绿线表示掩蔽区域的直方图。
使用均衡化直方图以获得更好的图像对比度
我们将学习直方图均衡化的概念,并使用它来改善我们的图像的对比度。
考虑这样一个图像,它的像素值仅被限制在某个特定的值范围内。例如,明亮的图像将所有像素限制在高值。但是一幅好的图像将包含图像所有区域的像素。所以你需要将直方图延伸到两端(如下图所示,来自维基百科),这就是直方图均衡化所做的(简单来说)。这通常会提高图像的对比度。
Numpy实现:
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的mask数组概念数组。对于mask数组,所有操作都是在非mask元素上执行的。你可以从Numpy文档中了解更多关于mask数组的信息。
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函数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链接。
我们刚刚看到的第一个直方图均衡化,考虑的是图像的全局对比度。在很多情况下,这不是一个好主意。例如下图是输入图像和全局直方图均衡化后的结果。
的确,经过直方图均衡化后,背景对比度得到了改善。但是比较两幅图中雕像的脸。由于亮度过高,我们丢失了大部分信息。这是因为它的直方图并不像我们在前面的例子中看到的那样局限于特定的区域(尝试绘制输入图像的直方图,你会得到更多的直觉)。
为了解决这一问题,采用了自适应直方图均衡化方法。在这里,图像被分割成小块为“tiles”(tileSize在OpenCV中默认为8x8)。然后这些块中的每一个都像往常一样直方图均衡化。所以在一个小的区域,直方图会局限在一个小的区域(除非有噪声)。如果噪音存在,它将被放大。为了避免这种情况,应用了对比限制。如果任何直方图bin高于指定的对比度限制(OpenCV默认为40),在应用直方图均衡化之前,这些像素将被裁剪并均匀分布到其他bin中。
均衡化后,利用双线性插值方法去除贴图边界中的伪影。
下面的代码片段展示了如何在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)
Also check these SOF questions regarding contrast adjustment:
在上面我们计算并绘制了一维直方图。它被称为一维,因为我们只考虑一个特征,即像素的灰度强度值。但在二维直方图中,我们考虑两个特征。通常它用于寻找颜色直方图,其中两个特征是每个像素的色相和饱和度值。
已经有一个用于查找颜色直方图的python例子(samples/python/color_histogram.py)。我们将尝试了解如何创建这样的颜色直方图,它将有助于理解进一步的主题,如直方图反向投影。
这非常简单,并且使用相同的函数cv.calcHist()进行计算。对于颜色直方图,我们需要将图像从BGR转换为HSV。(记住,对于一维直方图,我们从BGR转换为灰度)。对于二维直方图,其参数修改如下:
import numpy as np
import cv2 as cv
img = cv.imread('home.jpg')
hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV)
hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
Numpy还为此提供了一个特定的函数:np.histogram2d()。(记住,对于一维直方图,我们使用np.histogram())。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg')
hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV)
hist, xbins, ybins = np.histogram2d(h.ravel(),s.ravel(),[180,256],[[0,180],[0,256]])
第一个参数是H平面,第二个是S平面,第三个是每个bin的数量,第四个是它们的范围。
现在我们看看如何绘制这个颜色直方图。
我们得到的结果是一个大小为180x256的二维数组。因此,我们可以像往常一样使用cv.imshow()函数来显示它们。它将是一个灰度图像,从颜色看不出什么,除非你知道不同颜色的Hue值。
我们可以使用matplotlib.pyplot.imshow()函数绘制带有不同颜色映射的2D直方图。它让我们更好地了解不同的像素密度。但这也不能让我们第一眼就知道是什么颜色,除非你知道不同颜色的Hue值。但我还是喜欢这种方法。它简单但是更好。
注意:在使用这个函数时,请记住,使用最近邻插值,以获得更好的结果。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg')
hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV)
hist = cv.calcHist( [hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )
plt.imshow(hist,interpolation = 'nearest')
plt.show()
下图是输入图像及其颜色直方图。X轴表示S值,Y轴表示Hue。
在柱状图中,你可以看到H = 100和S = 200附近的一些高值。它对应着蓝色的天空。同样,在H = 25和S = 100附近也可以看到另一个峰。它对应着黄色的宫殿。你可以使用任何图像编辑工具(如GIMP)来验证它。
在OpenCV-Python2样本中有一个颜色直方图的示例代码(samples/python/color_histogram.py)。如果你运行该代码,你也可以看到柱状图显示相应的颜色。或者简单地说,它输出一个颜色编码的直方图。它的结果非常好(尽管你需要添加额外的行)。
在该代码中,作者用HSV创建了一个彩色map。然后转换成BGR。得到的直方图图像与这个颜色地图相乘。他还使用了一些预处理步骤来移除小的孤立像素,从而得到一个良好的直方图。
我把运行代码、分析代码并使用自己的方法留给读者。下面是与上面相同的图像的代码输出:
你可以清楚地看到柱状图中有什么颜色,蓝色在那里,黄色在那里,一些白色的棋盘在那里。不错! !
很明显,水平为S,竖直方向为H
(试了试效果,还不错)
这是由Michael J. Swain, Dana H. Ballard在他们的论文Indexing via color histograms中提出的。
简单来说,它到底是什么?它用于图像分割或在图像中寻找感兴趣的对象。简单地说,它创建一个与输入图像大小相同(但只有一个通道)的图像,其中每个像素对应于该像素属于我们的对象的概率。用更简单的话说,输出的图像中越白的区域越有可能是我们感兴趣的区域。这是一种直观的解释。(我不能再简单了)。直方图反投影与camshift算法等相结合。
我们怎么做呢?我们创造了包含我们感兴趣的对象(在我们的例子中,是地面,运动的球员和其他东西)的图像直方图。为了得到更好的效果,物体应该尽可能地填充图像。并且颜色直方图比灰度直方图更受欢迎,因为对象的颜色比灰度强度更好地定义对象。然后我们在测试图像上“反向投影”这个直方图,我们需要找到目标,换句话说,我们计算每个像素属于地面的概率并显示它。使用适当的阈值输出结果中就只有地面。
首先,我们需要计算我们需要找到的对象(设为M)和我们要搜索的图像(设为I)的颜色直方图。
import numpy as np
import cv2 as cvfrom matplotlib import pyplot as plt
#roi is the object or region of object we need to find
roi = cv.imread('rose_red.png')
hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
#target is the image we search in
target = cv.imread('rose.png')
hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
# Find the histograms using calcHist. Can be done with np.histogram2d also
M = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] ) # M和I都为180*256的图像
I = cv.calcHist([hsvt],[0, 1], None, [180, 256], [0, 180, 0, 256] )
求比值R=M/I。(I中能和M对应上的颜色,比值为1,其他地方为0)然后反投影 R,即使用R作为调色板,并创建一个新的图像,每个像素作为其相应的是目标的概率。即B(x,y) = R[h(x,y),s(x,y)],其中h为色调,s为像素在(x,y)处的饱和度。然后应用条件B(x,y)=min[B(x,y),1]。(注:结果为B(x,y)在[0,1]区间内,即B为和target大小一样的图,每一个像素值都是target中对应像素的概率值)
h,s,v = cv.split(hsvt)
B = R[h.ravel(),s.ravel()]
B = np.minimum(B,1)
B = B.reshape(hsvt.shape[:2])
现在与圆盘结构核进行卷积,B=D∗B,其中D是圆盘核。这一步仅仅是为了光滑?
disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(B,-1,disc,B)
B = np.uint8(B)
cv.normalize(B,B,0,255,cv.NORM_MINMAX)
现在最大强度的位置告诉了我们物体的位置。如果我们想知道物体在图像中的区域,选择合适的阈值进行二值化会得到一个很好的结果。
ret,thresh = cv.threshold(B,50,255,0)
OpenCV提供了一个内置函数 cv.calcBackProject()。它的形参几乎与cv.calcHist()函数相同。其中一个参数是直方图它是对象的直方图,我们必须找到它。同样,在传递给backproject函数之前,应该对对象直方图进行归一化。它返回概率图像。然后将图像与一个圆盘核进行卷积,并应用阈值。下面是我的代码和输出:
import numpy as np
import cv2 as cv
roi = cv.imread('rose_red.png')
hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
target = cv.imread('rose.png')
hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
# calculating object histogram
roihist = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )
# normalize histogram and apply backprojection
cv.normalize(roihist,roihist,0,255,cv.NORM_MINMAX)
dst = cv.calcBackProject([hsvt],[0,1],roihist,[0,180,0,256],1)
# Now convolute with circular disc
disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(dst,-1,disc,dst) # 光滑
# threshold and binary AND
ret,thresh = cv.threshold(dst,50,255,0)
thresh = cv.merge((thresh,thresh,thresh))
res = cv.bitwise_and(target,thresh)
res = np.vstack((target,thresh,res))
cv.imwrite('res.jpg',res)
下面是一个例子。我用蓝色矩形内的区域作为样本对象,想提取整个地面。
参考: https://docs.opencv.org/4.5.2/de/db2/tutorial_py_table_of_contents_histograms.html