(三)OpenCV中的图像处理之直方图
注释:本文翻译自OpenCV3.0.0 document->OpenCV-Python Tutorials,包括对原文档种错误代码的纠正
一.直方图-1:查找、绘制、分析
1.目标
- 使用OpenCV和Numpy函数查找直方图
- 使用OpenCV和matplotlib函数绘制直方图
- 会学会这些函数cv2.calcHist()、np.histogram()等
2.原理
那么什么是直方图呢?你可以将直方图视为图形或绘图,从而为你提供有关图像强度分布的总体思路。它是X轴上的像素值(范围从0到255,并非总是)和Y轴上图像中相应像素数量的绘图。
这只是理解图像的一种方式。通过查看图像的直方图,你可以直观了解该图像的对比度、亮度强度分布等。现在几乎所有的图像处理工具都提供了直方图上的特征。以下是来自Cambridge in Color网站的图片,我建议你访问该网站了解更多详情。
你可以看到图像及其直方图。(请记住,此直方图是灰度图像绘制的,而不是彩色图像)。直方图左侧区域显示图像中较暗像素的数量,右侧区域显示较亮像素的数量。从直方图中,你可以看到黑色区域不仅比明亮区域多,而且中间色调(中等范围内的像素值,例如127左右)的数量非常少。
3.查找直方图
现在我们对什么是直方图有了一个简单的了解,我们可以研究如何找到它。OpenCV和Numpy都有内置的函数。在使用这些函数之前,我们需要了解一些直方图相关的术语。
BINS:上面的直方图显示每个像素值的像素数量,即从0到255。也就是说,你需要256个值来显示上述的直方图。但是请考虑,如果您不需要分别查找所有像素值的像素数量,但是像素值的间隔中的像素数量是多少?例如,你需要找到位于10到15之间,然后是16到31,…,240到255之间的像素值。你只需要16个值来表示直方图。这就是OpenCV教程中关于直方图的例子。
因此,你所做的只是将整个直方图拆分成16个子部分,每个子部分的值是其中所有像素数的总和。这个子部分被称为“BIN”。在第一种情况下,BINS中的每组的像素数目都是256,而在第二种情况下,它仅为16个。在OpenCV中,BINS由术语hitSize表示。
DIMS:这是我们收集数据的参数数量。在这种情况下,我们只收集强度值的数量,所以这里是1.
RANGE:这是你要测量的强度值的范围。通常,它是[0,256],即所有强度值。
1)OpenCV中的直方图计算
我们使用cv2.calcHist()函数来查找直方图。让我们熟悉这个函数及其参数:
Cv2.calcHist(image,channels,mask,hitSize,range[,hist[,accumulate]])
- image(图像):它是类型为uint8或者float32的源图像。应该用方括号给出,即“[img]”.
- Channels(通道):它也是被放在方括号内。它是我们计算直方图的通道的索引。例如,如果输入是灰度图像,则其值为[0].对于彩色图像,可以分别通过[0],[1]或者[2]计算蓝色、绿色或者红色通道的直方图 。
- Mask(掩膜):要查找完整图像的直方图,它会显示为“无”。但是,如果你想找到特定区域的图像直方图,你必须为此创建一个蒙版图像并将其作为蒙版
- histSize:这代表我们的BIN数量。需要用方括号给出。对于全尺寸,我们传入[256].
- Range(范围):通常情况下,它是[0,256].
所以让我们从一个示例图像开始。只需在灰度模式下加载图像并找到完整的直方图。
import cv2
img = cv2.imread('1.jpg', 0)
hist = cv2.calcHist([img], [0], None, [256], [0, 256])
#hist是一个256*1的数组,每个值对应于图像中具有相应像素值的像素数
print(hist)
2)Numpy中的直方图计算
Numpy还提供了一个函数np.histogram().而不是calcHist()函数,你可以尝试下面的方式:
import cv2
import numpy as np
img = cv2.imread('1.jpg', 0)
hist, bins = np.histogram(img.ravel(), 256, [0, 256])
print("hist", hist)
print("bins", bins)
hist与我们之前计算的相同。但是bins将有257个元素因为Numpy计算bins为0-0.99,1-1.99,2-2.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函数。
4.绘制直方图
两种方法:
Ø Shortway:使用matplotlib绘图函数
Ø Long way:使用OpenCV绘图函数
1)使用Matplotlib
Matplotlib带有一个直方图绘制函数:matplot.pyplot.hist()
它直接找到直方图并绘制它.不需要使用calcHist()或者np.histogram()函数来查找直方图。请参阅下面的代码:
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('home.jpg',0)
plt.hist(img.ravel(),256,[0,256]); plt.show()
大概会得到下面的结果:
或者你可以使用matplotlib的正常绘制方式,这对BGR绘制是有利的.为此,你首先需要查找直方图数据。
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('8.jpg')
color = ('b', 'g', 'r')
for i, col in enumerate(color):
histr = cv2.calcHist([img], [i], None, [256], [0, 256])
plt.plot(histr, color=col)
plt.xlim([0, 256])
plt.show()
结果:
2)使用OpenCV
用OpenCV的话,你可以将直方图的值与其二进制一起调整为x,y坐标,以便你可以使用cv2.line()或cv2.polyline()函数绘制它以生成与上面相同的图像。这已经在OpenCV-Python2官方demo中可用。
5.掩膜的应用
我们使用cv2.calcHist()来查找完整图像的直方图。如果你想查找图像中某些区域的直方图,该怎么办?只需在想查找直方图的区域创建一个带白色的蒙版图像,否则就是黑色。然后将它作为掩膜。
# -*- coding: utf-8 -*-
'''
掩膜的应用:
'''
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('2.jpg')
# 创建一个掩膜
mask = np.zeros(img.shape[:2], np.uint8)
mask[100:300, 100:400] = 255
masked_img = cv2.bitwise_and(img, img, mask=mask)
# 计算有掩膜和没有掩膜时的直方图
# 只需改变第三个参数
hist_full = cv2.calcHist([img], [0], None, [256], [0, 256])
hist_mask = cv2.calcHist([img], [0], mask, [256], [0, 256])
plt.subplot(221), plt.imshow(img, 'gray'), plt.title("origianl")
plt.subplot(222), plt.imshow(mask, 'gray'), plt.title('mask')
plt.subplot(223), plt.imshow(masked_img, 'gray'), plt.title('masked_img')
plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask), plt.title('hist')
plt.xlim([0, 256])
plt.show()
二.直方图-2:直方图均衡
1.目标
学习直方图均衡化的概念,并使用它来提高图像的对比度
2.原理
考虑一个像素值仅限于某个特定范围的图像.例如,较亮的图像将所有像素限制在较高值。但是一张好的图像将具有来自图像所有区域的像素。因此,你需要将这个直方图拉伸到结束位置(如下图所示),这就是直方图均衡化所做的事。这通常会改变图像的对比度。
我建议你阅读直方图均衡化的*页面获取更多细节。通过编写例子,它有一个非常好的解释,以便在阅读完后几乎可以理解所有内容。相反,在这里我们将看到它的Numpy实现。之后,我们将看到它的OpenCV函数。
'''
直方图均衡化:用它来提高图像的对比度
实例代码如下
'''
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('4.jpg', 0)
hist, bins = np.histogram(img.flatten(), 256, [0, 256]) # img.flatten()将数组变为一维数组
cdf = hist.cumsum() # 计算直方图
cdf_normalized = cdf * hist.max() / cdf.max()
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]
cv2.imshow('original', img)
cv2.imshow('res', img2)
#plt.plot(cdf_normalized, color='b'), plt.hist(img.flatten(), 256, [0, 256], color='r'), plt.xlim([0, 256])
plt.plot(cdf, color='b'), plt.hist(img2.flatten(), 256, [0, 256], color='r'), plt.xlim([0, 256])
plt.legend(('cdf', 'histogram'), loc='upper left')
plt.show()
cv2.waitKey(0) & 0xFF
cv2.destroyAllWindows()
结果:
直方图均衡化另一个重要的特点是,即使图像是一个较暗的图像(而不是我们使用的较亮的图像),在均衡化之后,我们将获得与我们获得的图像几乎相同的图像。因此,这被用作“参考工具”来制作具有相同照明条件的所有图像。这在很多情况下很有用。例如,在面部识别中,在训练面部数据之前,面部图像被直方图均衡化,以使它们全部具有相同的照明条件。
3.OpenCV中的直方图均衡化(Histograms Equalization)
OpenCV中有另一个函数来做到这点,cv2.equalizeHist().它的输入是灰度图像,输出是直方图均衡图像。
下面是一个简单的代码片段,显示了我们使用相同图片的用法:
# -*- coding: utf-8 -*-
'''
OpenCV中的直方图均衡化
'''
import cv2
import numpy as np
img = cv2.imread('4.jpg', 0)
equ = cv2.equalizeHist(img)
res = np.hstack((img, equ)) # 将两张图片拼接到一起
cv2.imwrite('res.png', res)
cv2.imshow('res', res)
cv2.waitKey(0) & 0xFF
cv2.destroyAllWindows()
结果:
所以现在你可以在不同的光照条件下拍摄不同的图像,均衡它并检查结果。
当图像的直方图被限定在特定区域时,直方图均衡效果很好。在直方图覆盖大区域(即亮和暗像素都存在)的情况下,在强度变化较大的地方,它不会有效。请查看其它资源中的SOF链接。
4.CLAHE(对比度有限自适应直方图均衡化)
我们刚刚看到第一个直方图均衡好考虑了图像的全局对比度。在很多情况下,这不是一个好注意。例如,下图显示了全局直方图均衡化之后的输入图像及其结果。
在直方图均衡化之后,背景对比度已经得到改善。但比较两幅图像中雕像的面貌。由于亮度过高,我们丢失了大部分信息。这是因为它的直方图并不像我们在前面的例子中看到的那样局限于特定的区域(尝试绘制输入图像的直方图,你会得到更多灵感)。
所以为了解决这个问题,使用自适应直方图均衡。在这里,图像被分成称为“瓦片”的小块(在OpenCV中tileSize默认为8*8)。然后,每个这些块都像平常一样进行直方图均衡。因此,在一个小区域内,直方图会局限于——一个小区域(除非有噪声)。如果有噪声,它就会被放大。为了避免这种情况,应用对比度限制。如果任何直方图bin超过了指定的对比度限制(在OpenCV中默认为40),那么应用直方图均衡之前,这些像素将被剪切并均匀分配到其它bin。在均衡之后,为了出去瓦片边界中的伪影,应用双线性插值。
下面的代码中显示了如何在OpenCV中应用CLAHE(Contrast Limited Adaptive Histogram Equalization)
# -*- coding: utf-8 -*-
'''
自适应直方图均衡:
1.图像被分为称为'瓦片'的小块,(在OpenCV中listSize默认为8*8),每个块都像平常一样进行直方图均衡化
2.为了去除瓦片边界中的伪影,使用双线性插值
示例代码:
'''
import cv2
import numpy as np
img = cv2.imread('5.jpg', 0)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
cl1 = clahe.apply(img)
cv2.imwrite('clahe_2.jpg', cl1)
cv2.imshow('res', cl1)
cv2.waitKey(0) & 0xFF
cv2.destroyAllWindows()
结果:
三.直方图-3:二维直方图
1.目标
查找并绘制直方图,这在接下来的章节中有所帮助。
2.介绍
在第一篇文章中,我们计算并绘制了以为直方图。它被称为一维的因为我们仅考虑一个特征,即像素的灰度强度。但在二维直方图中,你会考虑两个特征。通常它用于查找颜色直方图,其中两个特征是每个像素的色调和饱和度值。
官方样本中已经有一个python样本用于查找颜色直方图。我们将尝试了解如何创建这样的颜色直方图,并且在理解直方图反投影这样的其它主题方面会很有用。
3.OpenCV中的2D直方图
使用cv2.calcHist()进行计算。对于颜色直方图,我们需要将图像从BGR转换为HSV。(请记住,对于一维直方图,我们从BGR转换为灰度)。对于2D直方图,其参数将被修改如下:
- Channels=[0,1],因为我们需要处理H和S平面
- Bins=[180,256],180是H平面,256是S平面
- Range=[0,180,0,256],色调介于0和180之间,饱和度介于0和256之间。
参考下面的代码:
import cv2
import numpy as np
img = cv2.imread('home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
4.Numpy中的2D直方图
Numpy中还提供了一个特定的函数:np.histogram2d()。(请记住,对于一维直方图,我们使用np.histogram()).
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist, xbins, ybins = np.histogram2d(h.ravel(),s.ravel(),[180,256],[[0,180],[0,256]])
第一个参数是H平面,第二个参数是S平面,第三个参数是每个参数的数量,第四个参数是参数的范围。
5.绘制2D直方图
方法1:用cv2.imshow()
我们得到的结果是一个尺寸为180*256的二维数组。所以我们可以像使用cv2.imshow()函数一样正常显示它们。这将是一个灰度图像,除非你知道不同颜色的色调值,否则不会给出太多颜色。
方法2:用matplotlib
我们可以用matplotlib.pyplot.imshow()函数来绘制不同颜色贴图的2D直方图。它让我们对不同的像素密度有了更好的想法。但是,这也不会让我们第一次看什么颜色,除非你想知道不同颜色的色调值。我仍然喜欢这种方法,它很简单更好。
注意:使用此函数时,请记住,插值标志应该最接近以获取更好的结果。
查看代码:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist = cv2.calcHist( [hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )
plt.imshow(hist,interpolation ='nearest')
plt.show()
方法3:OpenCV样本风格
在OpenCV-Python2示例中有一个颜色直方图示例的代码。如果你运行代码,你可以看到直方图也显示相应的颜色。或者只是输出一个颜色编码的直方图。它的结果非常好(尽管你需要添加更多的线)。
在该代码中,作者在HSV中创建了一张彩色地图。然后将其转换为BGR。得到的直方图图像与该颜色图相乘。他还使用了一些预处理步骤来移除小的孤立像素,从而产生良好的直方图。
四.直方图-4:直方图反投影
1.目标
学习直方图反射投影
2.原理
它是由Michael J. Swain , Dana H. Ballard在他们通过颜色直方图进行索引的论文中提出的。
简单地说,它用于图像分割或查找图像中感兴趣的对象,简而言之,它会创建与我们的输入图像相同大小(但单通道的)图像,其中每个像素对应于该像素属于对象的概率。再更简单地说,与其余部分相比,输出图像会使我们感兴趣的对象变得更白。那么,这是一个直观的解释。直方图反射投影与camshift算法一起使用。我们该怎么做呢?我们创建包含感兴趣的对象(在我们的例子中,地面,离开玩家和其它的一些东西)的图像的直方图。该对象应尽可能填充图像以获得更好的效果。颜色直方图比灰度直方图更受欢迎,因为对象的颜色是定义对象的更好方式,而不是其灰度强度。然后,我们在需要查找对象的测试图像上“反投影”这个直方图,即换句话说,我们计算每个像素属于地面并显示它的概率。在适当的阈值下得到的输出仅给我们提供了基础。
3.Numpy中的算法
查看代码及注释:
# -*- coding: utf-8 -*-
'''
直方图反投影:简单地说,它用于图像分割或查找图像中感兴趣的对象,反射投与camshift算法一起使用
'''
import cv2
import numpy as np
from matplotlib import pyplot as plt
"""
1.numpy中的算法:首先我们需要计算找到对象(定义为‘M’)和我们要搜索的图像(定义为‘I’)的直方图
"""
# roi是我们需要搜索的图像区域
roi = cv2.imread('roi.jpg')
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
# target是我们搜索的目标图像
target = cv2.imread("5.jpg")
hsvt = cv2.cvtColor(target, cv2.COLOR_BGR2HSV)
# 用calcHist找到直方图,也可以用np.histogram2d实现
M = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
I = cv2.calcHist([hsvt], [0, 1], None, [180, 256], [0, 180, 0, 256])
"""
第二是找到比率R=M/I,然后反投影R,即使用R作为调色板,并创建一个新图像
"""
R = M / I
h, s, v = cv2.split(hsvt) # h是色相,s是像素(x,y)处的饱和度
B = R[h.ravel(), s.ravel()]
B = np.minimum(B, 1)
B = B.reshape(hsvt.shape[:2])
'''
现在应用和圆盘的卷积,B=D*B,其中D是卷积核
'''
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
cv2.filter2D(B, -1, disc)
B = np.uint8(B)
cv2.normalize(B, B, 0, 255, cv2.NORM_MINMAX)
'''
现在最大强度的位置即是物体的位置。如果我们期待图像中的某个区域,则为合适的值设置阈值会给出更好的结果
'''
ret, thresh = cv2.threshold(B, 50, 255, 0)
cv2.imshow('res', ret)
cv2.waitKey(0) & 0xFF
cv2.destroyAllWindows()
结果:没跑出来。。。有buge
4.OpenCV中的反射投影
OpenCV提供了一个内置函数cv2.calcBackProject()。它的参数与cv2.calcHist()函数几乎相同。其中一个参数是直方图,它是对象的直方图,我们必须找到它。此外,对象直方图在传递到反投影函数之前应进行规范化。它返回概率图像,然后我们用卷积内核对图像进行卷积并应用阈值。以下是我的代码和输出:
# -*- coding: utf-8 -*-
'''
OpenCV中的直方图反射投影
1.OpenCV中提供了一个内置函数cv2.calcBackProject()
'''
import cv2
import numpy as np
roi = cv2.imread('roi.jpg')
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
target = cv2.imread('5.jpg')
hsvt = cv2.cvtColor(target, cv2.COLOR_BGR2HSV)
# 计算目标直方图
roihist = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
# 规格化直方图并应用反射投影
cv2.normalize(roihist, roihist, 0, 255, cv2.NORM_MINMAX)
dst = cv2.calcBackProject([hsvt], [0, 1], roihist, [0, 180, 0, 256], 1)
# 现在旋转圆盘
# 此处卷积可以把分散的点连在一起
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
dst = cv2.filter2D(dst, -1, disc, dst)
# 阈值和二进制and
ret, thresh = cv2.threshold(dst, 50, 255, 0)
# 别忘了是三通道图像,所以这里使用merge变为3通道,原文档代码还是错误的,参数应该是列表
thresh = cv2.merge([thresh, thresh, thresh])
# 按位操作
res = cv2.bitwise_and(target, thresh)
# 垂直堆叠图片 np.hstack()是水平堆叠图片 原文档代码错误的,这里的参数应该是列表
res = np.hstack([target, thresh, res])
cv2.imwrite('resProjection.jpg', res)
cv2.imshow('res', res)
cv2.waitKey(0) & 0xFF
cv2.destroyAllWindows()
# cv2.split()函数分离得到各个通道的灰度值(单通道图像);cv2.merge()函数是合并单通道成多通道(不能合并多个多通道图像)