OpenCV-Python教程:直方图及其绘制(calcHist)

原文链接:http://www.juzicode.com/opencv-python-histogram-calchist-draw-hist

返回Opencv-Python教程

图像的直方图反映的是图像像素值的统计特征,比如一个CV_8U类型的图像,表示的是其在0~255的256种数值的分布情况。我们可以将统计“颗粒度”划分在每一个像素值上,当然统计区间也可以不必在每一个像素值上划分,也可以将0-255平分成更宽的区间,比如0-7,8-15…..248-255每8个像素值作为一个区间来统计。在直方图中经常会遇到“bin”的概念,比如一个CV_8U的图像如果bin的尺寸设置为256,这样bin的宽度就为1,对应前面例子中在每一个像素值上统计,如果bin的尺寸设置为32,这样bin的宽度就为256/32=8,这样就会在每8个像素值上统计。

OpenCV里用calcHist()计算得到的直方图是一个矩阵(数组),虽然也是是一个二维图像,但是并不能直接用imshow()显示,需要经过转换配合绘制直线等方法将直方图表示成一幅直观的图像,另外也可以借助numpy和matplotlib绘制直方图。后者接口更简洁,稍后我们先来看看此方法。

1、matplotlib hist()绘制直方图

matplotlib中可以使用hist()方法绘制直方图,其接口形式:

hist(x, bins=None, range=None,......)
  • 参数含义:
  • x:输入序列,如果是二维图像需要展开为一维数组;
  • bins:柱子的多少,如果是CV_8U类型的图像设置为256,表示每个像素值为1个区间;
  • range:像素值的阈值范围,如果不设置会自动计算;

实际上matplotlib里的hist()方法入参有十几个,这里我们只需要使用上述几个参数就可以完成绘图。

下面的例子读入lena图,然后分别对其BGR通道进行直方图的绘制,绘制直方图时入参x要求为一维数组,所以使用ravel()方法将图像展开:

import numpy as np
import matplotlib.pyplot as plt
import cv2
plt.rc('font',family='Youyuan',size='9')
plt.rc('axes',unicode_minus='False')
print('VX公众号: 桔子code / juzicode.com')

img_src = cv2.imread('..\\lena.jpg')
b,g,r = cv2.split(img_src)  

#显示图像
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('b hist')
ax[0,0].hist(b.ravel(),bins=256)  
ax[0,1].set_title('g hist')
ax[0,1].hist(g.ravel(),bins=256)
ax[1,0].set_title('r hist')
ax[1,0].hist(r.ravel(),bins=256)
ax[1,1].set_title('src') 
ax[1,1].imshow(cv2.cvtColor(img_src,cv2.COLOR_BGR2RGB))  
#ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');
ax[1,1].axis('off')#关闭坐标轴显示
plt.show() 

运行结果:

OpenCV-Python教程:直方图及其绘制(calcHist)_第1张图片

在hist()方法中,设置bins=256,所以直方图的x方向的坐标长度为256,这时会统计每种像素值的像素个数。

hist()绘制的直方图,可以看做是bar()绘制的柱状图的一种特例,在直方图中柱子之间的间隔为0,x方向的坐标用数字代替了,可参考数据可视化~matplotlib饼图、柱状图。

2、计算直方图calcHist

calcHist()可以用来统计图像的直方图,接口形式:

cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]]) ->hist
  • 参数含义:
  • images:输入图像,是一个图像集合,可以是包含多通道彩色图像的list或tuple,也可以是多个灰度图组成的list或者tuple;list或tuple形式的输入;
  • channels:根据images确定,指明要用images里的哪个通道号,根据images的形式确定;list或tuple形式的输入;
  • mask:掩码;
  • histSize:直方图的尺寸,实际就是元素取值划分的等分;list或tuple形式的输入;
  • ranges:图像元素取值的范围;list或tuple形式的输入;
  • accumulate:如果为True表示多个图像时累积计算像素值个数;
  • hist:返回的直方图数据,是一个二维数组,数组形状为(histSize决定的行数,1);

下面这个例子对lena图的BGR通道分别计算直方图:

import numpy as np
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)

img_src = cv2.imread('..\\lena.jpg')
b,g,r = cv2.split(img_src)  
histSize = 256
histRange = (0, histSize)  #统计的范围和histSize保持一致时可覆盖所有取值
b_hist = cv2.calcHist([b], [0], None, [histSize], histRange)
g_hist = cv2.calcHist([g], [0], None, [histSize], histRange)
r_hist = cv2.calcHist([r], [0], None, [histSize], histRange)

print('b_hist.shape:',b_hist.shape)
min_max = cv2.minMaxLoc(b_hist)
print('b_hist.minMaxLoc:',min_max)
print('b_hist.非0数:',cv2.countNonZero(b_hist))
for i,v in enumerate(b_hist):
    print(v,end=' ')
    if (i+1)%16==0:print()

运行结果:

VX公众号: 桔子code / juzicode.com
cv2.__version__: 4.5.3
b_hist.shape: (256, 1)
b_hist.minMaxLoc: (0.0, 3260.0, (0, 0), (0, 95))
b_hist.非0数: 191
[0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.]
[0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [1.] [0.] [0.] [0.] [1.]
[1.] [0.] [3.] [2.] [6.] [8.] [2.] [12.] [16.] [34.] [42.] [33.] [64.] [74.] [123.] [148.]
[229.] [279.] [333.] [452.] [573.] [740.] [938.] [1137.] [1294.] [1616.] [1779.] [2091.] [2260.] [2464.] [2684.] [2690.]
[2732.] [2793.] [2807.] [2763.] [2782.] [2741.] [2610.] [2649.] [2710.] [2839.] [2981.] [2908.] [3101.] [3091.] [3102.] [3148.]
[3026.] [2967.] [3032.] [2851.] [2872.] [2776.] [2783.] [2818.] [2831.] [2970.] [2929.] [2959.] [3217.] [3209.] [3132.] [3260.]
[3253.] [3117.] [2999.] [2868.] [2785.] [2655.] [2628.] [2558.] [2620.] [2613.] [2614.] [2746.] [2775.] [2751.] [2661.] [2641.]
[2617.] [2591.] [2563.] [2571.] [2601.] [2792.] [2829.] [2862.] [3042.] [3190.] [3250.] [3225.] [3190.] [2933.] [2740.] [2422.]
[2197.] [1949.] [1754.] [1489.] [1302.] [1116.] [1045.] [968.] [848.] [863.] [863.] [883.] [878.] [837.] [848.] [862.]
[846.] [786.] [798.] [801.] [888.] [892.] [868.] [906.] [835.] [858.] [964.] [1018.] [976.] [1019.] [972.] [956.]
[885.] [965.] [948.] [929.] [919.] [821.] [856.] [838.] [777.] [755.] [779.] [741.] [719.] [698.] [618.] [581.]
[619.] [585.] [580.] [583.] [569.] [617.] [584.] [621.] [620.] [625.] [569.] [548.] [460.] [401.] [380.] [359.]
[324.] [267.] [200.] [201.] [134.] [138.] [130.] [125.] [118.] [99.] [115.] [82.] [57.] [58.] [51.] [45.]
[34.] [27.] [27.] [21.] [11.] [6.] [5.] [2.] [6.] [2.] [1.] [0.] [2.] [1.] [1.] [0.]
[0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.]
[0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.]

从b_hist.shape属性可以看到,b_hist是一个256行x1列的二维numpy数组,行数等于histSize=256。通过修改histSize的大小,可以看到b_hist.shape的属性随着histSize发生变化

histSize = 156
b_hist = cv2.calcHist([b], [0], None, [histSize], histRange)
print('b_hist.shape:',b_hist.shape)

-----运行结果:
b_hist.shape: (156, 1)

除了前面的例子中,images传入单个单通道图像组成的list、channels固定传入[0]的方式,images还可以使用单个多通道的图像,channels入参对应其通道号传入

img_src = cv2.imread('..\\lena.jpg')
#b,g,r = cv2.split(img_src)  
histSize = 256
histRange = (0, histSize)  #统计的范围和histSize保持一致时可覆盖所有取值
b_hist = cv2.calcHist([img_src], [0], None, [histSize], histRange) 
g_hist = cv2.calcHist([img_src], [1], None, [histSize], histRange)
r_hist = cv2.calcHist([img_src], [2], None, [histSize], histRange)

b,g,r = cv2.split(img_src)  
b_hist2 = cv2.calcHist([b], [0], None, [histSize], histRange) 
g_hist2 = cv2.calcHist([g], [0], None, [histSize], histRange)
r_hist2 = cv2.calcHist([r], [0], None, [histSize], histRange) 

print('b_hist差异:',cv2.countNonZero(cv2.absdiff(b_hist,b_hist2)))
print('g_hist差异:',cv2.countNonZero(cv2.absdiff(g_hist,g_hist2)))
print('r_hist差异:',cv2.countNonZero(cv2.absdiff(r_hist,r_hist2)))

运行结果:


b_hist差异: 0
g_hist差异: 0
r_hist差异: 0

从运行结果看,2种方式计算得到的直方图没有差异。在这个例子中入参“[img_src], [0], ”对应的是img_src的第0通道,对应了img_src的b通道。

images入参除了包含单个多通道彩色图像,还可以包含多个多通道彩色图像,这时channels的入参就会更复杂些,后面图像的通道号需要根据前面图像的通道号来叠加考虑,比如传入一个3通道的img_src1和一个3通道的img_src2:images=[img_src1,img_src2],这时计算img_src1的channels仍然分别取值为[0]、[1]、[2],img_src2的channels就需要在前一个图像通道的取值基础上叠加,分别取值为[3]、[4]、[5]。下面的例子来做一个验证,同时传入2个相同的3通道图像,这时其3,4,5通道的直方图应该要等于0,1,2通道的直方图:

import numpy as np 
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__) 

img_src = cv2.imread('..\\lena.jpg') 
histSize = 256
histRange = (0, histSize)  #统计的范围和histSize保持一致时可覆盖所有取值
b_hist = cv2.calcHist([img_src,img_src], [0], None, [histSize], histRange) 
g_hist = cv2.calcHist([img_src,img_src], [1], None, (histSize,), histRange)
r_hist = cv2.calcHist((img_src,img_src), [2], None, [histSize], histRange) 
#接下来的3,4,5通道号对应第2个输入图片的直方图
b_hist2 = cv2.calcHist((img_src,img_src), [3], None, [histSize], histRange) 
g_hist2 = cv2.calcHist((img_src,img_src), [4], None, [histSize], histRange)
r_hist2 = cv2.calcHist((img_src,img_src), [5], None, [histSize], histRange) 

print('b_hist差异:',cv2.countNonZero(cv2.absdiff(b_hist,b_hist2)))
print('g_hist差异:',cv2.countNonZero(cv2.absdiff(g_hist,g_hist2)))
print('r_hist差异:',cv2.countNonZero(cv2.absdiff(r_hist,r_hist2)))

运行结果:

b_hist差异: 0
g_hist差异: 0
r_hist差异: 0

以此类推,还可以有多种其他传入方法:

b,g,r = cv2.split(img_src)
histSize = 256
histRange = (0, histSize)  #统计的范围和histSize保持一致时可覆盖所有取值
b_hist = cv2.calcHist([img_src,b,g,r], [0], None, [histSize], histRange) 
g_hist = cv2.calcHist([img_src,b,g,r], [1], None, (histSize,), histRange)
r_hist = cv2.calcHist((img_src,b,g,r), [2], None, [histSize], histRange) 
#接下来的3,4,5通道号对应第2个输入图片的直方图
b_hist2 = cv2.calcHist((img_src,b,g,r), [3], None, [histSize], histRange) 
g_hist2 = cv2.calcHist((img_src,b,g,r), [4], None, [histSize], histRange)
r_hist2 = cv2.calcHist((img_src,b,g,r), [5], None, [histSize], histRange) 

3、calcHist()计算 matplotlib plot()显示

前面介绍了matplotlib hist()方法直接显示直方图,这里利用calHist()计算出直方图,得到的是一个数组,该数组的下标表示像素值代表x轴,数组元素的值表示该下标对应的像素值个数代表y轴,所以也可以利用matplotlib的plot()方法绘制直方图:

import numpy as np
import matplotlib.pyplot as plt
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
plt.rc('axes',unicode_minus='False')

img_src = cv2.imread('..\\lena.jpg')
b,g,r = cv2.split(img_src)  
histSize = 256
histRange = (0, histSize) 统计的范围和histSize保持一致时可覆盖所有取值
b_hist = cv2.calcHist([b], [0], None, [histSize], histRange) 
g_hist = cv2.calcHist([g], [0], None, [histSize], histRange) 
r_hist = cv2.calcHist([r], [0], None, [histSize], histRange) 

#显示图像
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('b hist')
ax[0,0].plot(b_hist) 
ax[0,1].set_title('g hist')
ax[0,1].plot(g_hist)
ax[1,0].set_title('r hist')
ax[1,0].plot(r_hist)
ax[1,1].set_title('src') 
ax[1,1].imshow(cv2.cvtColor(img_src,cv2.COLOR_BGR2RGB))  
#ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');
ax[1,1].axis('off')#关闭坐标轴显示
plt.show() 

运行结果:

OpenCV-Python教程:直方图及其绘制(calcHist)_第2张图片

该方法绘制的直方图和matplotlib的hist()方法绘制曲线的分布和走势是一样的。

4、OpenCV绘图显示直方图

calcHist()计算得到的直方图名义上是“图”,但是并不能直接用OpenCV的imshow()显示,需要做转换才能显示,直方图是一个histSize行x1列的二维数组,其第2维是只包含一个元素的numpy数组,比如取b_hist的第55个元素的值:

print('b_hist[55]:',b_hist[55])
print('int(b_hist[55]):',int(b_hist[55]))
-----运行结果:
b_hist[55]: [122.07056]
int(b_hist[55]): 122

这里用int()方式取整后,直接将numpy数组转换成了int型。这样数组下标55就代表了其x轴的取值,取整后的122就代表了y轴的取值。

下面的例子绘制lena图BGR通道的直方图,用calcHist()计算完BGR通道的直方图后,创建一个hist_img_w,hist_img_h = 512,350大小的numpy数组用来保存可视化的直方图图像img_hist。BGR通道直方图数据值归一化到img_hist的高度,这样做以免绘图时超出了图像边界。然后以histSize宽度为循环边界,每次用line()方法绘制hist_img_w/histSize个宽度的直线:

import numpy as np
import cv2 
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)

img_src = cv2.imread('..\\lena.jpg') 
histSize = 256
histRange = (0, histSize)  
b_hist = cv2.calcHist([img_src], [0], None, [histSize], histRange) 
g_hist = cv2.calcHist([img_src], [1], None, [histSize], histRange) 
r_hist = cv2.calcHist([img_src], [2], None, [histSize], histRange) 
#创建直方图空图像
hist_img_w,hist_img_h = 512,350  
img_hist = np.zeros((hist_img_h, hist_img_w, 3), dtype=np.uint8)
#归一化到0和直方图显示的高度
cv2.normalize(b_hist, b_hist, alpha=0, beta=hist_img_h, norm_type=cv2.NORM_MINMAX)
cv2.normalize(g_hist, g_hist, alpha=0, beta=hist_img_h, norm_type=cv2.NORM_MINMAX)
cv2.normalize(r_hist, r_hist, alpha=0, beta=hist_img_h, norm_type=cv2.NORM_MINMAX)
#绘图,以histSize宽度为循环边界,每次绘制bin_w个宽度
bin_w = int(round( hist_img_w/histSize ))
print('bin_w',bin_w)
for i in range(1, histSize):
    cv2.line(img_hist, 
            ( bin_w*(i-1), hist_img_h - int(b_hist[i-1]) ),#起始点位置
            ( bin_w*(i)  , hist_img_h - int(b_hist[i]) ),  #结束点位置
            ( 255, 0, 0), thickness=2)
    cv2.line(img_hist, 
            ( bin_w*(i-1), hist_img_h - int(g_hist[i-1]) ),
            ( bin_w*(i)  , hist_img_h - int(g_hist[i]) ),
            ( 0, 255, 0), thickness=2)
    cv2.line(img_hist, 
            ( bin_w*(i-1), hist_img_h - int(r_hist[i-1]) ),
            ( bin_w*(i)  , hist_img_h - int(r_hist[i]) ),
            ( 0, 0, 255), thickness=2)
cv2.imshow('img_src', img_src)
cv2.imshow('img_hist', img_hist)
cv2.waitKey()

画出来的直方图是这样的:

OpenCV-Python教程:直方图及其绘制(calcHist)_第3张图片

5、2D直方图

2D直方图仍然使用calcHist()计算,入参形式和一维直方图类似但稍有差异。

从前面介绍一维直方图的例子来看,calcHist()使用时channels入参都只有1个元素表明输入图像的某一个通道,而计算2D直方图则需要指明2个通道,并且images参数所表示的图像必须是多个通道的。同时histSize参数增加到2个,histSize[0]对应channels[0]通道的直方图尺寸,histSize[1]对应channels[1]通道的直方图尺寸。histRange参数增加到4个,histRange[0]和[1]对应channels[0]通道的取值范围,histRange[2]和[3]对应channels[1]通道的取值范围。下面是一个计算lena图像HSV色彩空间中H和S分量2D直方图的例子:

import numpy as np
import cv2 
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)

img_src = cv2.imread('..\\lena.jpg') 
img_hsv = cv2.cvtColor(img_src,cv2.COLOR_BGR2HSV)
img_hist = cv2.calcHist( [img_hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )
print('img_hist.shape:',img_hist.shape)
#归一化到255
minmax=cv2.minMaxLoc(img_hist)
img_hist2 = (255*img_hist/minmax[1]).astype(np.uint8)
#显示
cv2.imshow('img_hist', img_hist)
cv2.imshow('img_hist2', img_hist2)
cv2.waitKey()

在这个例子中channels=[0, 1],取其中的H、S分量计算直方图;histSize=[180, 256],表示H分量histSize为180,S分量的histSize为256;histRange=[0, 180, 0, 256],H分量的histRange为0~180,S分量的histRange为0~256。

运行结果:

OpenCV-Python教程:直方图及其绘制(calcHist)_第4张图片

小结:在一维直方图里x方向表示像素值大小的取值,y方向表示的是该像素值有多少的取值(包含像素值的量的多少),在x轴的左侧表示更暗的像素的多少,x轴的右侧表示更亮的像素的多少。除了通常意义上表示的亮度(灰度级),如果将图像转换为HSV色彩空间,也可以用来表示饱和度、色度的直方图。二维直方图的调用形式在入参上和一维直方图类似,二维直方图可以直接使用imshow()方法显示,

扩展阅读:

​​​​​​有了这个方法群聊斗图你就不会输了

新鲜上架的Python3.10,来个match-case尝尝鲜

你别耍我,0.1+0.2居然不等于0.3?

如何实现一个“万能”的调试打印函数

论如何把自己变成卡通人物

有了这款神器,什么吃灰文件都统统现形

一行代码深度定制你的专属二维码(amzqr)

桔子菌和超市老板田大爷的一次角色互换经历

你可能感兴趣的:(图像处理,OpenCV,数字图像,计算机视觉,opencv)