OpenCV Tutorials 15 - 理解图像特征以及Harris角点检测

2,354 阅读8分钟

Understanding Features & Harris Corner Detection

本小节主要介绍什么是图像特征,为什么图像特征很重要还有为什么拐角很重要等

一、说明

大多数人都玩过拼图游戏。 您会得到很多图像的小片段,您需要将它们正确组合以形成大的真实图像。 问题是,你是怎么做到的? 将相同的理论投射到计算机程序中以便计算机可以玩拼图游戏怎么样? 如果计算机可以玩拼图游戏,为什么我们不能把很多好的自然风景的真实图像交给计算机,让它把所有这些图像拼接成一个大的图像呢? 如果计算机可以将几张自然图像拼接到一张上,那么给出大量建筑物或任何结构的图片并告诉计算机从中创建一个 3D 模型呢?

好吧,问题和想象仍在继续。 但这一切都取决于最基本的问题:您如何玩拼图游戏? 你如何将大量打乱的图像片段排列成一个大的图像? 如何将大量自然图像拼接到单个图像中?

答案是,我们正在寻找独特的、易于跟踪和易于比较的特定模式或特定特征。 如果我们去定义这样一个特征,我们可能会发现很难用语言来表达它,但我们知道它们是什么。 如果有人要求您指出一项可以在多张图像之间进行比较的好功能,您可以指出一项。 这就是为什么即使是小孩也可以简单地玩这些游戏。 我们在图像中搜索这些特征,找到它们,在其他图像中寻找相同的特征并对齐它们。 就是这样。 (在拼图游戏中,我们更多地关注不同图像的连续性)。 所有这些能力都存在于我们内在。

所以我们的一个基本问题在数量上扩展到更多,但变得更具体。 这些功能是什么? (对于计算机来说,答案也应该是可以理解的。)

1. 示例

很难说人类是如何找到这些特征的。 这已经在我们的大脑中进行了编程。 但是如果我们深入一些图片并搜索不同的模式,我们会发现一些有趣的东西。 例如,以下图为例:

download.jpg

图像非常简单。在图像的顶部,给出了六个小图像块。您的问题是在原始图像中找到这些小型区域的确切位置。你能找到多少正确的结果? A 和 B 是平面,它们分布在很多区域。很难找到这些小型区域的确切位置。 C和D要简单得多。它们是建筑物的边缘。您可以找到一个大概的位置,但确切的位置仍然很困难。这是因为沿边缘的所有地方的图案都是相同的。然而,在边缘,它是不同的。因此,与平坦区域相比,边缘是更好的特征,但还不够好(在拼图游戏中比较边缘的连续性很好)。最后,E 和 F 是建筑物的一些角落。而且很容易找到。因为在拐角处,无论您将这个小型区域移动到哪里,它看起来都会有所不同。所以它们可以被认为是好的特性。所以现在我们进入更简单(和广泛使用的图像)以便更好地理解。

download.png

就像上面一样,蓝色小型区域是平坦的区域,很难找到和跟踪。 无论您将蓝色小型区域移动到哪里,它看起来都是一样的。 黑色小型区域有一个边缘。 如果您在垂直方向(即沿渐变)移动它,它会发生变化。 沿着边缘移动(平行于边缘),它看起来是一样的。 而对于红色小型区域,它是一个角落。 无论您将小型区域移动到哪里,它看起来都不同,这意味着它是独一无二的。 所以基本上,角落被认为是图像中的好特征。 (不仅仅是角落,在某些情况下,团状物也可被认为是好的特性)。

2. 结论

所以现在我们回答了我们的问题,“这些特征是什么?”。 但是下一个问题出现了。 我们如何找到它们? 或者我们如何找到角落? 我们以直观的方式回答了这个问题,即在图像中的所有区域中寻找在移动(少量)时具有最大变化的区域。 这将在接下来的章节中投射到计算机语言中。 所以找到这些图像特征被称为特征检测。

我们发现了图像中的特征。 找到它后,您应该能够在其他图像中找到相同的内容。 这是怎么做到的? 我们在特征周围取一个区域,我们用我们自己的话来解释它,例如“上半部分是蓝天,下半部分是建筑物的区域,在那栋建筑物上有玻璃等”,然后您在另一个中搜索相同的区域 图片。 基本上,您正在描述该功能。 同样,计算机也应该描述特征周围的区域,以便它可以在其他图像中找到它。 所谓的描述称为特征描述。 获得特征及其描述后,您可以在所有图像中找到相同的特征并将它们对齐、拼接在一起或做任何您想做的事情。

所以在这个模块中,我们正在寻找 OpenCV 中的不同算法来查找特征、描述它们、匹配它们等。

二、哈里斯角点检测

本小节中我们会学习:

  1. 哈里斯角点检测的概念
  2. 用到以下函数:cv.cornerHarris(), cv.cornerSubPix()
  • 用法如下:
  1. cv.cornerHarris( src, blockSize, ksize, k[, dst[, borderType]] ) -> dst
  2. cv.cornerSubPix( image, corners, winSize, zeroZone, criteria ) -> corners

三、概念

上一小节中,我们了解到了图像中的角点区域是那些在任一方向上移动都会产生极大变化的区域。1988年,Chris Harris 和 Mike Stephens 在他们的论文 a Combined Corner and Edge Detector 中对这些角点进行了早期的尝试,所以现在被称为 Harris Corner Detector。他把这个简单的想法变成了数学形式。它基本上求出了(u,v)在所有方向上位移的强度差。详情如下:

E(u,v)=x,yw(x,y)[I[x+u,y+v]I(x,y))]2E(u,v) = \sum_{x,y}w(x,y)[I[x+u,y+v] - I(x,y))]^2,其中 w(x,y)w(x,y)为滤波(窗口)函数,I(x,y)I(x,y)为位移强度

窗口函数是一个矩形窗口或者是一个高斯窗口,它给位于窗口内的像素赋权。

我们必须最大化这个函数 E(u,v) 来进行角点检测。 这意味着我们必须最大化第二项。 将泰勒展开式应用于上述方程并使用一些数学步骤(请参阅您喜欢的任何标准教科书以获得完整推导),我们得到最终方程:

E(u,v)[uv]M[uv]E(u,v) \approx \begin{bmatrix} u & v \end{bmatrix} M \begin{bmatrix} u \\ v \end{bmatrix}, 此时, M=x,yw(x,y)[IxIxIxIyIxIyIyIy] M = \sum_{x,y}w(x,y)\begin{bmatrix} I_xI_x & I_xI_y \\ I_xI_y & I_yI_y \end{bmatrix}

其中,IxI_xIyI_y 分别是 x 和 y 方向的图像导数。 (这些可以很容易地使用 cv.Sobel() 得到)。 然后是主要部分。 在此之后,他们创建了一个分数,基本上是一个灯饰,它决定了一个窗口是否可以包含一个角。

R=det(M)k(trace(M))2R = det(M) - k(trace(M))^2

其中,det(M)=λ1λ2,trace(M)=λ1+λ2det(M) = \lambda_1\lambda_2,trace(M) = \lambda_1+\lambda_2,λ1\lambda_1λ2\lambda_2是矩阵M的特征值

因此,这些特征值的大小决定了一个区域是角、边缘还是平面:

  1. R|R|很小的时候(也就是λ1\lambda_1λ2\lambda_2都很小),这个区域就是平面
  2. 当 R < 0(也就是λ1\lambda_1远大于λ2\lambda_2,反之亦然),这个区域就是边缘
  3. 当 R 很大(也就是λ1\lambda_1λ2\lambda_2都很大,且两值相差很小),这个区域就是角点

download.jpg

所以 Harris Corner Detection 的结果是具有这些分数的灰度图像。 为合适的分数设置阈值可为您提供图像中的角点。 我们将使用一个简单的图像来完成。

1. OpenCV中的Harris角点检测

主要用到以下函数:cv.cornerHarris( src, blockSize, ksize, k[, dst[, borderType]] ) -> dst

  1. img - 输入图像。 它应该是灰度和float32类型。
  2. blockSize - 角点检测考虑的邻域大小
  3. ksize - 使用的 Sobel 导数的孔径参数。
  4. k - 方程中的 Harris 检测器自由参数。
import cv2 as cv
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
def cv_show(name, img):
    cv.imshow(name, img)
    cv.waitKey(0)
    cv.destroyAllWindows()
def compare(imgs):
  #  for i in range(len(imgs)):
 #       imgs[i][:,-3:-1,:] = [255,255,255]
    res = np.hstack(imgs)
    cv_show('Compare', res)
# 读取原图
img  = cv.imread('chessboard.jpg')
# 转化为灰度图
gray = cv.cvtColor(img ,cv.COLOR_BGR2GRAY)
# 转化阵列数据类型
gray = np.float32(gray)
# 检测角点
res = cv.cornerHarris(gray, 2, 3 ,0.04)

# 使用膨胀操作让角点更加显眼
res = cv.dilate(res,None)

# 修改原图,绘制出角点信息
img[res > 0.01*res.max()] = [0, 0, 255]

cv_show("result", img)

download.png

2. 对视频进行角点检测

cap  = cv.VideoCapture('playchess.mp4')

if not cap.isOpened():
    exit()
    
while 1:
    ret,frame = cap.read()
    if not ret:
        break
#     gray = np.float32(cv.cvtColor(frame, cv.COLOR_BGR2GRAY))
    
#     res = cv.cornerHarris(gray, 5, 3, 0.04)
#     res = cv.dilate(res,None)
    
#     frame[res > res.max()*0.04] = [0,0,255]
#     cv.imshow('Result', frame)
    gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    res = cv.cornerHarris(gray, 5, 5, 0.04)
    res = cv.dilate(res,None)
    
    frame[res > res.max()*0.04] = [0,0,255]
    cv.imshow('Result', frame)
    # cv.imshow('Result', gray)
    if cv.waitKey(1)&0xff == 27:
        break
cv.destroyAllWindows()
cap.release() 

download.png

3. 具有亚像素精度的角点

有时,您可能需要找到最准确的拐角。 OpenCV 带有一个函数 cv.cornerSubPix(),它进一步细化了以亚像素精度检测到的角点。 下面是一个例子。 像往常一样,我们需要先找到哈里斯角点。 然后我们通过这些角的质心(一个角可能有一堆像素,我们取它们的质心)来细化它们。 哈里斯角用红色像素标记,细化角用绿色像素标记。 对于这个函数,我们必须定义何时停止迭代的标准。 我们在指定的迭代次数或达到一定的精度后停止它,以先发生者为准。 我们还需要定义它搜索角落的邻域的大小。

  • cv.cornerSubPix( image, corners, winSize, zeroZone, criteria ) -> corners
  • cv.connectedComponentsWithStats( image[, labels[, stats[, centroids[, connectivity[, ltype]]]]] ) -> retval, labels, stats, centroids
  • cv.connectedComponentsWithStatsWithAlgorithm( image, connectivity, ltype, ccltype[, labels[, stats[, centroids]]] ) -> retval, labels, stats, centroids
img  = cv.imread('Outlook.png')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

gray = np.float32(gray)
res = cv.cornerHarris(gray, 2, 3, 0.04)
res = cv.dilate(res,None)
# 将得到的结果灰度图像进行二值化
ret, dst = cv.threshold(res,0.01*res.max(),255,0)
dst = np.uint8(dst)
# 找出角点重心
ret, labels, stats, centroids = cv.connectedComponentsWithStats(dst)
# 定义停止迭代以及重新提取角点的标准
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 100, 0.001)
corners = cv.cornerSubPix(gray,np.float32(centroids),(5,5),(-1,-1),criteria)
# Now draw them
res = np.hstack((centroids,corners))
res = np.int0(res)
img[res[:,1],res[:,0]]=[0,0,255]
img[res[:,3],res[:,2]] = [0,255,0]
cv_show('Result', img)

下面是结果,其中一些重要位置显示在缩放窗口中以进行可视化:

download.png

download.png