OpenCV中的图像处理5-直方图

查找、绘制和分析

直方图可以总体了解图像的强度分布,是在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]])查找直方图。

  1. images:它是uint8或float32类型的源图像,它应该放在方括号中,即“[img]”。
  2. channels:以方括号给出,它是计算直方图的通道的索引。如输入为灰度图像,则其值为[0];彩色图像,可传递[0]、[1]、[2]以分别用于计算蓝色、绿色、红色通道的直方图。
  3. mask:图像掩码。为了找到完整图像的直方图,将其指定为“无”;若要查找图像特定区域的直方图,则必须为此创建一个掩码图像并将其作为掩码。
  4. histSize:表示BIN计数,对于全尺寸,为[256]。
  5. 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中的算法

  1. 计算要查找的对象(使其为“M”)和被搜索的图像(使其为“I”)的颜色直方图。
  2. 求出比值 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]
  3. 对圆盘应用卷积,B = D * B,其中D是圆盘内核
  4. 现在最大强度的位置给了要查找对象的位置,若期望被搜索的图像中有一个区域,则对合适的值进行阈值处理将获得不错的结果。

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中文文档

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值