目录
查找、绘制和分析
直方图可以总体了解图像的强度分布,是在X轴上具有像素值(不总是从0~255),在Y轴上具有图像中相应像素数的图。通过查看图像的直方图,可以直观地了解该图像的对比度、亮度、强度分布等。
寻找直方图
一些与直方图有关的术语:
BINS:有的直方图显示每个像素值的像素数,即从0到255,需要256个值来显示这种直方图。若不需要分别找到所有像素值的像素数,而是找到像素值间隔中的像素数,如找到介于0~15之间的像素数,然后是16~31之间,…,240~255之间,则只需要16个值即可表示直方图;将直方图分成16个子部分,每个子部分的值是其中所有像素数的总和,每个子部分都称为"BIN";BINS由OpenCV文档中的histSize术语表示。
DIMS:为其收集数据的参数的数量。在某种情况下,仅收集关于强度值一件事的数据,DIMS是1。
RANGE:要测量的强度值的范围,通常它是[0, 256],即所有强度值。
1. OpenCV中的直方图计算
可使用函数cv.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])
查找直方图。
- images:它是uint8或float32类型的源图像,它应该放在方括号中,即“[img]”。
- channels:以方括号给出,它是计算直方图的通道的索引。如输入为灰度图像,则其值为[0];彩色图像,可传递[0]、[1]、[2]以分别用于计算蓝色、绿色、红色通道的直方图。
- mask:图像掩码。为了找到完整图像的直方图,将其指定为“无”;若要查找图像特定区域的直方图,则必须为此创建一个掩码图像并将其作为掩码。
- histSize:表示BIN计数,对于全尺寸,为[256]。
- ranges:表示RANGE,通常为[0, 256]。
2. numpy的直方图计算
Numpy提供函数np.histogram()
查找直方图,如下代码:
hist, bins = np.histogram(img.ravel(), 256, [0, 256])
bin将具有257个元素,因为Numpy计算出bin的范围为0-0.99、1-1.99等,因此最终范围为255-255.99,为了表示这一点,在最后添加了256。
Numpy还有另一个函数np.bincount()
,它比np.histogram()
快10倍左右。
hist = np.bincount(img.ravel(), minlength = 256)
对于一维直方图,可以更好的尝试一下。
OpenCV函数比np.histogram()
快大约40倍,因此尽可能使用OpenCV函数。
绘制直方图
1. 使用Matplotlib
Matplotlib带有直方图绘图功能:matplotlib.pyplot.hist()
,它直接找到直方图并将其绘制,无需使用cv.calcHist()
或np.histogram()
函数来查找直方图。
2. 使用OpenCV
可以调整直方图的值及其bin值,使其看起来像x, y坐标,以便使用cv.line()
或cv.polyline()
函数绘制它以生成与 Matplotlib绘制的相同的图像。
find_plot_hist.py
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('./OpenCV/fusi1.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
r_hist = cv.calcHist([img], [2], None, [256], [0, 256]) #红色通道的直方图
nphist, npbins = np.histogram(img.ravel(), 256, [0, 256])
nphist2 = np.bincount(img.ravel(), minlength=256)
plt.subplot(3, 3, 1)
plt.plot(r_hist)
plt.subplot(3, 3, 2)
plt.plot(nphist)
plt.subplot(3, 3, 3)
plt.plot(nphist2)
plt.subplot(3, 3, 4)
plt.hist(gray.ravel(), 256, [0, 256])
color = ('b', 'g', 'r')
plt.subplot(3, 3, 5)
for i, col in enumerate(color):
histr = cv.calcHist([img], [i], None, [256], [0, 256])
plt.plot(histr, color = col)
plt.xlim([0, 256])
# 创建一个掩码
mask = np.zeros(gray.shape[:2], np.uint8)
mask[100:300, 100:400] = 255
masked_img = cv.bitwise_and(gray, gray, mask=mask)
# 计算掩码区域和非掩码区域的直方图,检查作为掩码的第三个参数
hist_full = cv.calcHist([img], [0], None, [256], [0, 256])
hist_mask = cv.calcHist([img], [0], mask, [256], [0, 256])
images = [gray, mask, masked_img]
for j in range(3):
plt.subplot(3, 3, j + 6), plt.imshow(images[j], 'gray')
plt.subplot(3, 3, 9)
plt.plot(hist_full), plt.plot(hist_mask)
plt.xlim([0, 256])
plt.show()
直方图均衡
某些图像,它的像素值仅局限于某个特定的值范围,如较亮的图像将把所有像素限制在高值上。但是一幅好的图像会有来自图像所有区域的像素,因此需要将直方图拉伸到两端,这就是直方图均衡化的作用,这通常会提高图像的对比度。
Numpy实现直方图均衡化
若一个图像直方图位于较暗(或亮)的区域,需要全频谱;为此需要一个转换函数,将暗(或亮)区域的输入像素映射到整个区域的输出像素,这就是直方图均衡化的作用。
可找到最小的直方图值(不包括0),应用直方图均衡化方程。使用掩码数组,得到有关每个输入像素值的输出像素值是什么的信息,然后应用变换:img2 = cdf[img]
然后计算转换后的直方图和cdf,并画出
hist_cdf.py
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
def draw_cdf_hist(img, index):
hist, bins = np.histogram(img.flatten(), 256, [0, 256]) #计算直方图
cdf = hist.cumsum() #计算cdf
cdf_normalized = cdf * float(hist.max()) / cdf.max()
plt.subplot(2, 2, 1 + index)
plt.imshow(img), plt.xticks([]), plt.yticks([])
plt.subplot(2, 2, 2 + index)
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')
return cdf
img = cv.imread('./OpenCV/fusi1.jpg', 0)
cdf = draw_cdf_hist(img, 0)
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]
draw_cdf_hist(img2, 2)
plt.show()
OpenCV中的直方图均衡
OpenCV具有执行此操作的功能cv.equalizeHist()
,它的输入是灰度图像,输出是直方图均衡图像。可以在不同的光照条件下拍摄不同的图像,对其进行均衡并检查结果。当图像的直方图限制在特定区域时,直方图均衡化效果很好;在直方图覆盖较大区域(即同时存在亮像素和暗像素)的强度变化较大的地方,效果不好。
CLAHE(对比度受限的自适应直方图均衡)
在一些情况下,均衡化考虑图像的整体对比度,效果不理想,直方图均衡后,背景对比度可能得到改善,但某些位置可能由于亮度过高,导致丢失一些信息。为了解决这个问题,使用自适应直方图均衡。在这种情况下,图像被分成称为“tiles”的小块(OpenCV中,tileSize默认为8x8);然后对每一个块进行直方图均衡,因此在较小的区域中,直方图被限制在一个较小的区域中(除非有噪声),若有噪声,噪声会被放大;为了避免这种情况,应用对比度限制;如果任何直方图bin超出指定的对比度限制(在OpenCV中默认为40),则在应用直方图均衡之前,将这些像素裁剪并均匀地分布到其他bin;均衡后,要消除图块边界中的伪影,应用双线性插值;
equalize_hist.py
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('./OpenCV/statue.png', 0)
equ = cv.equalizeHist(img)
clahe = cv.createCLAHE(clipLimit = 2.0, tileGridSize = (8, 8))
cl1 = clahe.apply(img)
images = [img, equ, cl1]
titles = ['Original', 'Global Equalize', 'CLAHE']
for i in range(3):
hist = cv.calcHist([images[i]], [0], None, [256], [0, 256])
plt.subplot(2, 3, i + 1)
plt.plot(hist)
plt.subplot(2, 3, i + 4)
plt.imshow(images[i], 'gray'), plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
二维直方图
上面学习了计算和绘制一维直方图,称为一维直方图是由于仅考虑了一个特征,即像素的灰度强度值;在二维直方图中,要考虑两个特征。
通常二维直方图用于查找颜色直方图,其中两个特征是每个像素的色相和饱和度值。
OpenCV中的二维直方图
也使用函数cv.calcHist()
进行计算,对于颜色直方图,需将图像从BGR转换为HSV,参数将进行如下修改:
- channel = [0, 1],同时处理H和S平面
- bins = [180, 256],对于H平面为180,S平面为256
- range = [0, 180, 0, 256],色相值介于0和180之间,饱和度介于0和256之间。
Numpy中的二维直方图
使用函数np.histogram2d(h.ravel(),s.ravel(),[180,256],[[0,180],[0,256]])
,第一个参数是H平面,第二个是S平面,第三个是每个bin的数量,第四个是它们的范围。
绘制二维直方图
1. 使用cv.imshow()
得到的二维直方图是尺寸为80x256的二维数组,可以使用函数cv.imshow()
显示,它将是一幅灰度图像。
2. 使用Matplotlib
可以使用matplotlib.pyplot.imshow()
函数绘制具有不同颜色图的2D直方图,它使我们对不同的像素密度有了更好的了解。注意:使用此功能时,插值法应采用最近邻以获得更好的结果。
3. OpenCV示例样式
在HSV中创建一个颜色图,将其转换为BGR,将所得的直方图图像与此颜色图相乘;还可以使用一些预处理步骤来删除小的孤立像素,从而获得良好的直方图(未找到参考实现)。
hist2d.py
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('./OpenCV/fusi1.jpg')
hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
h, s, v = cv.split(hsv)
nhist, xbins, ybins = np.histogram2d(h.ravel(), s.ravel(), [180, 256], [[0, 180], [0, 256]])
hists = [hist, nhist]
plt.subplot(2, 2, 1)
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.subplot(2, 2, 2)
plt.imshow(hist, interpolation='nearest')
for i in range(2):
plt.subplot(2, 2, i + 3)
plt.plot(hists[i])
plt.show()
直方图反投影
它用于图像分割或在图像中查找感兴趣的对象。简而言之,它创建的图像大小与输入图像相同(但只有一个通道),其中每个像素对应于该像素属于我们物体的概率。用更简单的话来说,与其余部分相比,输出图像将在可能有对象的区域具有更多的白色值。直方图反投影与camshift算法等配合使用。
创建一个图像的直方图,其中包含感兴趣的对象(在示例中是背景,离开播放器等)。对象应尽可能填充图像以获得更好的效果,而且颜色直方图比灰度直方图更可取,因为对象的颜色对比灰度强度是定义对象的好方法。然后,将该直方图“反投影”到需要找到对象的测试图像上,换句话说,计算出属于背景的每个像素的概率并将其显示出来。在适当的阈值下产生的输出可以仅获得背景。
Numpy中的算法
- 计算要查找的对象(使其为“M”)和被搜索的图像(使其为“I”)的颜色直方图。
- 求出比值
R
=
M
I
R = \dfrac{M}{I}
R=IM,然后反向投影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 = D * B,其中D是圆盘内核
- 现在最大强度的位置给了要查找对象的位置,若期望被搜索的图像中有一个区域,则对合适的值进行阈值处理将获得不错的结果。
OpenCV的反投影
OpenCV提供了一个内建的函数cv.calcBackProjiect()
,它的参数几乎与cv.calcHist()
函数相同;第一个参数是直方图,也就是物体的直方图。在传递给backproject函数之前,应该对对象直方图进行归一化,它返回概率图像,然后使用圆盘内核对图像进行卷积并应用阈值。
hist_backprojection.py
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# roi为需要找到的对象或对象区域
roi = cv.imread('./OpenCV/appleRoi.png')
hsv = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
# target为被搜索的图像
target = cv.imread('./OpenCV/fruit.jpg')
thsv = cv.cvtColor(target, cv.COLOR_BGR2HSV)
# Numpy反投影相关
# 使用calcHist查找直方图,或者使用np.histogram2d
M = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
# h, s, v = cv.split(thsv)
# I, xbins, ybins = np.histogram2d(h.ravel(), s.ravel(), [180, 256], [[0, 180], [0, 256]])
I = cv.calcHist([thsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
# 计算反向投影R
R = cv.divide(M, I)
h, s, v = cv.split(thsv)
B = R[h.ravel(), s.ravel()]
B = np.minimum(B, 1)
B = B.reshape(thsv.shape[:2])
# 用圆盘进行卷积
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)
npret, npthresh = cv.threshold(B, 50, 255, 0)
npthresh = cv.merge((npthresh, npthresh, npthresh))
npres = cv.bitwise_and(target, npthresh)
# OpenCV的反投影,使用到M和thsv、圆盘disc
# 直方图M归一化并利用反传算法
cv.normalize(M, M, 0, 255, cv.NORM_MINMAX)
cvdst = cv.calcBackProject([thsv], [0, 1], M, [0, 180, 0, 256], 1)
# 用圆盘进行卷积
cv.filter2D(cvdst, -1, disc, cvdst)
# 应用阈值作与操作
cvret, cvthresh = cv.threshold(cvdst, 50, 255, 0)
cvthresh = cv.merge((cvthresh, cvthresh, cvthresh))
cvres = cv.bitwise_and(target, cvthresh)
imgs = [target, roi, B, npres, cvthresh, cvres]
titles = ['Original', 'ROI', 'B', 'Result', 'CVThresh', 'CVResult']
for i in range(6):
plt.subplot(2, 3, i + 1)
plt.imshow(cv.cvtColor(imgs[i], cv.COLOR_BGR2RGB))
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
代码中使用R = M / I可能出现警示信息:
RuntimeWarning: divide by zero encountered in true_divide
R = M / I
RuntimeWarning:invalid value encountered in true_divide
R = M / I
学习来源:OpenCV-Python中文文档