基于Python的OpenCV图像处理13

Jun 11,2017   6024 words   22 min


2D直方图

在前面我们学习了绘制普通的直方图,x轴是图像的灰度级数,如256,y轴是某一灰度级对应的像素数或出现的概率。 在这过程中,我们只考虑的图像的灰度信息这一个特征,因此称作一维,对应的就是一维直方图。那么对于二维而言, 自然是需要考虑两个特征,这两个特征分别是:颜色(Hue)和饱和度(Saturation)。我们根据这每个像素的这两个特征绘制直方图。 同时可以发现,H和S是HSV空间里的特征,所以在绘制之前需要把BGR图像转换为HSV图像,这很简单,在前面已经说过了。

计算2D直方图

与之前一维直方图的计算类似,有两种方法可以使用。一种是基于OpenCV,另一种是基于Numpy。

基于OpenCV

在之前使用函数cv2.calcHist()计算直方图,这里计算2D直方图还是使用它。但是需要修改一些参数。函数原型如下:

cv2.calcHist(images,channels,mask,histSize,ranges[,hist[,accumulate]])

利用该函数得到的是一个180×256维的二维数组,其中每个位置对应的值表示像素个数。 例如(100,100)的值是500,表示H为100,S为100的像素在图像中一共有500个。 如何理解也很简单。因为H是180,S是256, 我们需要画二维直方图,所以就需要以H、S作为变量,统计图像中对应像素的个数。

参数解释如下:

  • images:待处理的图像,图像格式为uint8或float32,传入时用[ ]括起来,如[img]

  • channels:对应图像需要统计的通道,若是灰度图则为0,彩色图像B、G、R对应0、1、2,注意同样需要用[ ]括起来,如[0]

  • mask:掩膜图像。如果统计整幅图像就设置为None,否则这里传入设计的掩膜图像。

  • histSize:前面说过了,BIN的数目。同样用[ ]括起来,如[256]

  • ranges:像素量化范围,通常为0 - 255。

在之前,我们只对一个通道进行统计,所以channels是[0],现在需要对H、S通道统计,所以就是[0,1]。 同理bins应该写为[180,256],range为[0,180,0,255]。OpenCV中H的取值范围在0到180,S、V的取值范围在0到256。 示例代码如下:

# coding=utf-8
import cv2

img = cv2.imread("E:\\060702.jpg")

img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# 统计二维直方图,注意有两个通道
hist = cv2.calcHist([img_hsv], [0, 1], None, [180, 256], [0, 180, 0, 255])
print hist.shape
基于Numpy

Numpy同样提供了统计二维直方图的函数histogram2d(),一维直方图可以用histogram()。示例代码如下:

# coding=utf-8
import cv2
import numpy as np

img = cv2.imread("E:\\060702.jpg")

img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# 调用时要注意Numpy接收的参数是一维的,而我们直接获取到的H、S是二维的,
# 所以需要使用ravel函数将二维数组展开
# 第一个参数是H通道
# 第二个参数是S通道
# 第三个参数是bins的数目,灰度级数
# 第四个参数是灰度变化范围
# 返回有三个参数
# 第一个参数是180*256的二维数组
# 第二个参数是x轴单独的统计结果(H)
# 第三个参数是y轴单独统计的结果(S)
# 其中第二个、第三个结果可以不接收
hist, xbins, ybins = np.histogram2d(img_hsv[0].ravel(), img_hsv[1].ravel(), [180, 256], [[0, 180], [0, 255]])
print hist.shape
print xbins.shape
print ybins.shape
绘制2D直方图

在前面利用OpenCV和Numpy对图像进行统计得到2D直方图数据以后,下面就是需要把它绘制出来。绘制同样有两种方法,基于 OpenCV和Matplotlib。但OpenCV并不适合绘制图表,而且方法比较复杂,所以这里只介绍基于Matplotlib的。

基于Matplotlib

我们可以使用Matplotlib中的imshow()函数进行绘制。在使用时,需要将插值参数设置为”nearest”。代码如下:

# coding=utf-8
import cv2
from matplotlib import pyplot as plt



img = cv2.imread("E:\\060702.jpg")

img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

hist = cv2.calcHist([img_hsv], [0, 1], None, [180, 256], [0, 180, 0, 255])

plt.imshow(hist, interpolation='nearest')
plt.show()

下图为原图和绘制结果,X轴是S,Y轴是H。可以看到x=100,y=105附近有比较高的峰值。 我们可以截取这部分继续放大观察。

直方图反向投影

英文叫Histogram Backprojection。 所谓反向投影就是首先计算某一特征的直方图模型,然后使用模型去寻找图像中存在的该特征。 那么既然是反向,那什么是正向呢?其实我们之前做的一维直方图以及 二维直方图都可以看作是一种正向投影。就是根据图片经过某种算法可以得到一个直方图。而且同样一张 影像投影后得到的直方图是相同的。由图像到直方图的过程可以认为是一种正向投影,那么反过来由直方图 到影像的过程可以认为是一种反向投影。在反向投影中,我们需要先求出原图直方图, 然后得到反向投影矩阵,由直方图到反向投影矩阵实际上就是个反向过程。 那么直方图反向投影可以干什么呢?又是怎么从直方图到图像的呢?

首先反向投影用于在输入图像(通常较大)中查找特定图像(通常较小或者仅1个像素,以下将其称为模板图像)最匹配的点或者区域, 也就是定位模板图像出现在输入图像的位置。用于在图像中查找指定特征。 它可以用来做图像分割或者在图像中寻找我们感兴趣的部分。简单来说,他会输出与输入图像相同大小的影像。 它直接取直方图中的值,即以灰度为例,某种灰度值在整幅图像中所占面积越大,其在直方图中的值越大, 反向投影时,其对应的像素的新值越大(越亮),反过来,某灰度值所占面积越小,其新值就越小。 其中每一个像素值代表了输入图像上对应点属于目标对象的概率。更直观地表达就是,输出图像中像素值越高(越白)的 点就越可能代表我们要搜索的目标。它经常与Camshift算法一起使用。

那么它是怎么实现的呢?从原理上来说,首先,我们需要为一张包含我们要查找目标的图像创建直方图。最好使用颜色直方图, 因为一个物体的颜色要比它的灰度能更好的被用来进行图像分割与对象识别。接着我们把这个颜色直方图投影到 输入图像中寻找我们的目标,也就是找到输入图像中每一个像素点的像素值在直方图中对应的概率,这样我们就 得到一个概率影像。最后设置适当的阈值对图像进行二值化,便完成操作。

反向投影原理
直方图反向投影矩阵计算

在理解反向投影原理前,首先要了解反向投影矩阵式如何计算的。 假设某幅图像的灰度如下所示: 首先我们可以对它统计灰度直方图。由于这里为了计算简便, 将0-255划分成如下区间,分别统计在各个区间有多少个像素。 统计完灰度直方图后,下面便是反向投影矩阵的核心了。 对于(0,0)像素,其灰度值为43,对应灰度直方图中的数量 为5。所以把5赋给(0,0)位置。同理(0,1)值为183,对应灰度 直方图中的数量为4。所以把4赋给(0,1)。重复可得到下面灰度矩阵: 下面需要思考的是,反向投影矩阵的这些数值有什么意义。 其实矩阵中的值就是某个灰度区间或灰度值对应在原图中的像素个数! 如果把灰度值都除以原图像中的像素总数,那么得到的就是某个灰度值 或某个灰度区间在原图中出现的概率。 用统计学术语,输出图像象素点的值是观测数组在某个分布(直方图)下的概率。 概率越高值越高,对应像素就会越白。所以我们上面说,得到的反向投影图 反映的是原图中是某个物体的概率大小。概率越高像素越白。 其实这是一个对图像简化的过程,而所谓简化其实就是提取特征的过程。 如果说两幅图像的反向投影矩阵相似或相同,那就意味着他们的灰度直方图 分布是相同或相似的。根据前面的结论,如果两幅图像直方图相同或相似, 那么这两幅图像有很大可能有相同或相似的特征。但至于是不是相同或相似, 需要另作判断。因为直方图忽略了一个很重要的因素——像素位置。最简单的例子如下: 两幅图像大小相同,都有同样大小的白色方块,只是位置不同。 这样它们的灰度直方图以及反向投影图是一模一样的,如下图。 我们可以判断它们两个有相似的特征(都有白色方块),但无法确定两幅图片相同(无法确定是否有相同位置)。 这虽然看起来有些不够“彻底”,无法判断两幅图像是否相同。但是从另一个角度来说, 这其实是实现了图像搜索功能。即只要图像中有相似的特征,不管在哪,都可以识别并找到。 我们可以基于此进行简单的目标识别与提取。

反向投影步骤

从编程实现的角度来说,步骤主要如下。就是不断的在输入图像中切割跟模板图像大小一致的图像块,并用直方图对比的方式与模板图像进行比较。 假设我们有一张100x100的输入图像,有一张10x10的模板图像,查找的过程是这样的:

  • (1)从输入图像的左上角(0,0)开始,切割一块(0,0)至(10,10)的临时图像;
  • (2)生成临时图像的直方图;
  • (3)用临时图像的直方图和模板图像的直方图对比,对比结果记为c;
  • (4)直方图对比结果c,就是结果图像(0,0)处的像素值;
  • (5)切割输入图像从(0,1)至(10,11)的临时图像,对比直方图,并记录到结果图像;
  • (6)重复(1)~(5)步直到输入图像的右下角。

反向投影的结果包含以每个输入图像像素点为起点的直方图对比结果。可以把它看成是一个二维的浮点型数组,二维矩阵,或者单通道的浮点型图像。 如果输入图像和模板图像一样大,那么反向投影相当于直方图对比。如果输入图像比模板图像还小,则会报错。

代码实现
基于OpenCV

在OpenCV中提供了cv2.calcBackProject()函数用于反向投影,它与cv2.calcHist()函数参数基本相同。 其中一个参数是我们要查找目标的直方图。同样在使用目标直方图做反向投影之前,需要先对其进行归一化。 该函数返回结果是一个概率图像,再使用圆盘形卷积核对其进行操作,然后设定阈值进行二值化。代码如下:

# coding=utf-8
import cv2


# 定义一个空回调函数
def nothing(x):
    pass


# 分别依次读取样本图像coke_roi以及待提取的图像coke
coke = cv2.imread("E:\\coke.png")
coke_roi = cv2.imread("E:\\coke_roi.png")

# 将两幅图像转成HSV空间
coke_hsv = cv2.cvtColor(coke, cv2.COLOR_BGR2HSV)
coke_roi_hsv = cv2.cvtColor(coke_roi, cv2.COLOR_BGR2HSV)

# 首先对样本图像计算2D直方图
coke_roi_hsv_hist = cv2.calcHist([coke_roi_hsv], [0, 1], None, [180, 256], [0, 180, 0, 255])

# 对得到的样本2D直方图进行归一化
# 这样可以方便显示,归一化后的直方图就变成0-255之间的数了
# cv2.NORM_MINMAX表示对数组所有值进行转换,线性映射到最大最小值之间
cv2.normalize(coke_roi_hsv_hist, coke_roi_hsv_hist, 0, 255, cv2.NORM_MINMAX)

# 对待检测图像进行反向投影
# 最后一个参数为尺度参数
dst = cv2.calcBackProject([coke_hsv], [0, 1], coke_roi_hsv_hist, [0, 180, 0, 256], 1)

# 构建一个圆形卷积核,用于对图像进行平滑,连接分散的像素
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
dst = cv2.filter2D(dst, -1, disc)
# 显示概率图像
cv2.imshow("Possibility", dst)

# 新建一个窗口用于观察结果
cv2.namedWindow("result")
# 设置阈值默认为50,及若某像素是目标的概率大于50,则认为是目标
cv2.createTrackbar('Threshold', 'result', 50, 100, nothing)

while 1:
    # 实时获取拖动条对应的阈值
    threshold = cv2.getTrackbarPos('Threshold', 'result')

    # 对反向投影后的概率图像根据阈值进行二值化
    ret, thresh = cv2.threshold(dst, threshold, 255, 0)

    # 注意由于原图是三通道BGR图像,因此在进行位运算之前,先要把thresh转成三通道
    thresh = cv2.merge((thresh, thresh, thresh))

    # 对原图与二值化后的阈值图像进行位运算,得到结果
    res = cv2.bitwise_and(coke, thresh)

    cv2.imshow('result', res)

    k = cv2.waitKey(1) & 0xff
    if k == 27:
        break

效果如下: 以上代码实现了对图片中某一个物体的提取。 第一幅图是待提取图片的原图以及样本ROI(橙色方框标出)。 第二幅图是通过反向投影得到的概率图。 第三幅图是设置不同概率阈值而得到的不同识别结果。 可以看到在阈值设置的比较高的时候,提取结果都是目标,正确率很高,但提取的完整性稍微差一些。 当阈值设置的较小时,整个目标的提取比较完整,但是正确率下降了,出现了很多非目标点。 因此如果不在乎提取目标的完整性,那么可以将阈值设高,以获得更精确的结果。 如果只是为了对目标进行粗提取,之后还有其它精细步骤,比较在乎完整性,那么可以将阈值设低一些。 注意这里将图片转成HSV空间然后使用2D直方图进行提取的。 因为前面说了,2D直方图可以反映物体的颜色,相较于灰度直方图可以更好反映特征。

本文作者原创,未经许可不得转载,谢谢配合

返回顶部