在计算机视觉领域,特征点检测与匹配是解决多种问题的核心,包括图像识别、跟踪、三维重建和运动分析。OpenCV作为一个功能强大的视觉处理库,提供了丰富的功能来处理这些任务。本博客旨在提供一个关于OpenCV中特征点检测与匹配方法的快速入门指南。
我们将从角点检测开始,探讨如Harris、Shi-Tomasi和FAST等经典算法,介绍它们的原理、公式和代码实现。接着,我们将深入到特征点检测的高级话题,覆盖如SIFT、SURF和ORB等算法。每种方法都会有详细的函数解析,帮助理解其背后的工作原理。最后,我们将讨论特征点匹配技术,包括BF匹配器、FLANN匹配器和RANSAC匹配方法,它们在处理不同图像间的特征点对应关系时至关重要。
角点检测(Corner Detection)是计算机视觉和图像处理中的一个基本概念,指的是在图像中识别出具有明显角点特征的点。在OpenCV中,我们通常使用Harris 角点检测或Shi-Tomasi角点检测来实现这一功能。
角点特征是指在图像中那些局部特征明显、在多个方向上具有变化的点。在计算机视觉中,角点是图像中最重要的特征之一,因为它们通常对图像的变化(如视角、光照、尺度等)保持不变性。角点特征在图像处理、模式识别、三维建模、运动追踪等领域有着广泛的应用。
- 局部特征:角点是图像中局部特征的重要表示,能够代表图像的关键信息。
- 不变性:它们相对稳定,对光照、旋转、缩放等图像变化具有一定的抵抗力。
- 高信息量:角点包含了丰富的信息,适合用于图像匹配、目标跟踪等。
import cv2
import random
# 读取图像
image = cv2.imread('tulips.jpg')
# 生成随机关键点
keypoints = []
num_keypoints = 50 # 假设我们想生成50个关键点
for _ in range(num_keypoints):
x = random.randint(0, image.shape[1] - 1)
y = random.randint(0, image.shape[0] - 1)
keypoints.append(cv2.KeyPoint(x, y, 1))
# 使用 drawKeypoints 绘制关键点
keypoint_image = cv2.drawKeypoints(image, keypoints, None, color=(0, 255, 0), flags=0)
# 显示图像
cv2.imshow('Random Keypoints', keypoint_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
在这个示例中,我创建了50个随机位置的关键点。cv2.KeyPoint
需要 x 和 y 坐标以及关键点的大小(这里设置为 1)。这些点将以绿色标记在图像上。
drawKeypoints
函数是 OpenCV 库中用于在图像上绘制关键点的一个函数。这个函数的目的是将检测到的关键点可视化,使得这些点在图像上更容易被识别。
def drawKeypoints(image, keypoints, outImage, color=None, flags=None)
image: 这是源图像,即你想在其上绘制关键点的图像。这个图像应该是一个标准的 OpenCV 图像格式,通常是使用
cv2.imread
读取的。keypoints: 这个参数是一个关键点的列表。这些关键点通常是通过特征检测算法(如 SIFT, SURF, ORB 等)得到的。每个关键点通常包含了图像中一个特定点的位置(x, y 坐标)和其他信息(如方向、大小等)。
outImage: 这是输出图像。函数会在这个图像上绘制关键点,并将其返回。如果这个参数是 None,OpenCV 通常会直接在源图像上绘制关键点。
color: 这个可选参数定义了关键点的颜色。如果未指定,将使用默认颜色。颜色通常以 (B, G, R) 格式指定,其中 B、G、R 分别代表蓝色、绿色和红色的强度。
flags: 这个可选参数定义了绘制关键点时的一些特性。OpenCV 提供了几个不同的标志来控制如何绘制关键点。
例如,cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
会绘制关键点的大小和方向,
而cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS
将不会绘制单个的关键点。
Harris角点检测的基本原理是观察窗口移动时,窗口内像素灰度变化的程度。具体来说,它计算了窗口在各个方向上移动时灰度变化的二阶矩阵,并通过此矩阵来判断一个点是否为角点。角点的特征是无论窗口向哪个方向移动,窗口内的灰度变化都很大。
基本检测原理如下:
- 假设有一个小窗口(或掩模),在整个图像上移动。
- 对于窗口中的每个像素,我们希望了解当窗口在各个方向上移动小距离时,像素强度的变化情况。
- 强度变化可以用函数 E ( u , v ) E(u,v) E(u,v)来描述,其中 u u u 和 v v v 是窗口在x和y方向上的移动量。
Harris角点检测的核心是计算每个像素的Harris响应值。数学公式为:
E ( u , v ) = ∑ x , y w ( x , y ) [ I ( x + u , y + v ) − I ( x , y ) ] 2 E(u,v) = \sum_{x,y} w(x, y)[I(x + u, y + v) - I(x, y)]^2 E(u,v)=x,y∑w(x,y)[I(x+u,y+v)−I(x,y)]2
其中, I ( x , y ) I(x, y) I(x,y) 是像素点 (x, y) 的强度, w ( x , y ) w(x, y) w(x,y) 是窗口函数(通常是高斯窗口),用于赋予窗口中心附近的像素更高的权重。
这个公式可以通过泰勒级数展开并简化,最终表示为:
E ( u , v ) ≈ [ u , v ] M [ u , v ] T E(u,v) ≈ [u, v] M [u, v]^T E(u,v)≈[u,v]M[u,v]T
其中, M M M 是由图像的梯度导数计算得出的 2 × 2 2 \times 2 2×2矩阵。
Harris响应值的计算可以通过以下步骤进行:
- 需要计算图像在x和y方向的梯度,通常用Sobel算子来完成。这可以表示为 I x I_x Ix 和 I y I_y Iy。
- 计算梯度的乘积 I x 2 , I y 2 I_x^2, I_y^2 Ix2,Iy2 和 I x I y I_xI_y IxIy。
- 对这些乘积应用高斯滤波(窗口函数),以在局部区域内平滑它们。这里使用高斯窗口是为了给图像的局部区域赋予更多的权重。
使用上述结果构造Harris矩阵(也称为结构张量),其一般形式为:
M = ( ∑ I x 2 ∑ I x I y ∑ I x I y ∑ I y 2 ) M = \begin{pmatrix} \sum I_x^2 & \sum I_xI_y \\ \sum I_xI_y & \sum I_y^2 \end{pmatrix} M=(∑Ix2∑IxIy∑IxIy∑Iy2)
其中,求和表示在窗口内对应像素值的求和。
计算每个像素的Harris响应值 R:
R = det ( M ) − k ⋅ ( trace ( M ) ) 2 R = \text{det}(M) - k \cdot (\text{trace}(M))^2 R=det(M)−k⋅(trace(M))2
其中, det ( M ) \text{det}(M) det(M)是矩阵M的行列式, trace ( M ) \text{trace}(M) trace(M)是矩阵的迹(即对角线元素的和),k是一个经验常数(通常介于0.04到0.06之间)。
为了理解这个公式如何与M的特征值相关联,我们可以考虑矩阵M的特征分解。矩阵M的两个特征值 λ 1 \lambda_1 λ1 和 λ 2 \lambda_2 λ2 描述了图像在该点的两个主要方向的梯度强度。矩阵M的行列式是这两个特征值的乘积:
det ( M ) = λ 1 ⋅ λ 2 \text{det}(M) = \lambda_1 \cdot \lambda_2 det(M)=λ1⋅λ2
而矩阵的迹是这两个特征值的和:
trace ( M ) = λ 1 + λ 2 \text{trace}(M) = \lambda_1 + \lambda_2 trace(M)=λ1+λ2
因此,Harris响应值R可以重写为:
R = λ 1 λ 2 − k ( λ 1 + λ 2 ) 2 R = \lambda_1 \lambda_2 - k (\lambda_1 + \lambda_2)^2 R=λ1λ2−k(λ1+λ2)2
- 根据Harris响应值R,可以判断每个像素是否是角点:
- 如果 R R R 很大,那么该点可能是角点。
- 如果 R R R 很小或接近零,那么该点可能是平坦区域。
- 如果 R R R 为负值,那么该点可能是边缘。
Harris角点检测限制:
尽管Harris角点检测是一种强大的工具,但它也有局限性。例如,它不是尺度不变的,这意味着在图像尺度显著变化时,检测到的角点可能会变化。此外,它对噪声比较敏感,并且在处理高度结构化的图像时可能会产生误检。
在OpenCV中,可以通过cv2.cornerHarris()
函数实现Harris角点检测。下面是一个简单的示例代码:
import cv2
import numpy as np
# 读取图像
img = cv2.imread('tulips.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Harris角点检测
gray = np.float32(gray)
dst = cv2.cornerHarris(gray, blockSize=2, ksize=3, k=0.04)
# 结果阈值化以确定角点位置
dst = cv2.dilate(dst, None)
thresh = 0.01 * dst.max()
img[dst > thresh] = [0, 0, 255]
# 绘制圆圈标记角点
for i in range(dst.shape[0]):
for j in range(dst.shape[1]):
if dst[i, j] > thresh:
# 绘制圆圈
cv2.circle(img, (j, i), radius=2, color=(0, 255, 0), thickness=1)
# 显示图像
cv2.imshow('Harris Corners', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
在这段代码中,cv2.cornerHarris()
函数接受几个参数:输入图像、邻域大小(blockSize)、用于边缘检测的Sobel算子的孔径大小(ksize)和Harris检测器的自由参数(k)。函数返回每个像素的Harris响应值。通过设置一个阈值,可以选择性地显示角点。
cornerHarris
用于角点检测的一个函数。它实现了 Harris 角点检测算法,该算法能够在图像中有效地识别角点。
def cornerHarris(src, blockSize, ksize, k, dst=None, borderType=None)
src (Source Image): 输入图像,它应该是单通道(灰度)的,数据类型可以是 8 位或者浮点型。这是函数进行处理的原始图像。
blockSize: 它指的是用于角点检测的邻域大小。简单来说,它是在每个像素周围考虑用于角点检测的像素块的大小。较大的块将考虑更多的像素,可能更适合于检测大的角点特征。
ksize (Kernel Size): Sobel算子的孔径参数。这个参数决定了用于计算图像梯度的Sobel卷积核的大小。这直接影响到图像梯度的计算,进而影响角点检测的结果。
k: Harris检测器的自由参数,用于在响应函数中加权角点的测量。它影响了角点检测的敏感度,通常在 0.04 到 0.06 之间。
dst (Destination Image): 用于存储 Harris 检测器响应的图像。它的类型是
CV_32FC1
,与输入图像src
有相同的大小。borderType: 像素外推法的类型。在进行图像梯度计算时,需要对边缘像素周围进行像素值的外推。这个参数指定了所使用的外推方法。常见的外推方法包括反射外推、常量外推等。
函数的主要功能是在输入图像上运行 Harris 角点检测器。它会计算每个像素的响应值,这些响应值可以用来确定图像中的角点位置。角点可以被识别为这些响应值的局部最大值。
Shi-Tomasi角点检测方法是基于Harris角点检测的一种改进,它在多个方面提供了更好的性能和精度。Shi-Tomasi方法的核心思想是通过评估图像的最小特征值来识别角点。
Shi-Tomasi方法的基本原理与Harris角点检测类似,都是基于图像局部区域的自相关矩阵。不同之处在于,Shi-Tomasi方法使用了自相关矩阵的最小特征值来评估角点的质量。
对于图像中的每个点,首先计算其周围区域的自相关矩阵。然后,计算该矩阵的两个特征值(记为 λ 1 \lambda_1 λ1和 λ 2 \lambda_2 λ2)。在Shi-Tomasi方法中,如果这两个特征值都大于某个阈值,则该点被认为是角点。
Shi-Tomasi方法的角点响应函数定义为:
R = min ( λ 1 , λ 2 ) R = \min(\lambda_1, \lambda_2) R=min(λ1,λ2)
其中, λ 1 , λ 2 \lambda_1, \lambda_2 λ1,λ2是自相关矩阵的特征值。
在OpenCV中,Shi-Tomasi角点检测可以通过 cv2.goodFeaturesToTrack()
函数实现。以下是一个简单的示例代码:
import cv2
# 读取图像
img = cv2.imread('tulips.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Shi-Tomasi角点检测
corners = cv2.goodFeaturesToTrack(gray, maxCorners=100, qualityLevel=0.01, minDistance=10)
# 绘制角点
for corner in corners:
x, y = corner.ravel()
cv2.circle(img, (int(x), int(y)), 5, (0, 255, 0), -1)
# 显示图像
cv2.imshow('Shi-Tomasi Corners', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
在这段代码中:
cv2.goodFeaturesToTrack()
函数接收几个参数:输入图像、要检测的角点最大数量(maxCorners
)、角点质量等级(qualityLevel
,一个介于 0 到 1 之间的数,代表最小特征值的相对值)和角点之间的最小距离(minDistance
)。(x, y)
坐标的形式返回。goodFeaturesToTrack
用于角点检测的函数,它实现了 Shi-Tomasi 方法。下面是该函数各个参数的简要说明:
def goodFeaturesToTrack(image, maxCorners, qualityLevel, minDistance, corners=None, mask=None, blockSize=None, useHarrisDetector=None, k=None)
image: 输入的图像。它应该是单通道的8位或者浮点型32位图像。
maxCorners: 要返回的角点的最大数量。如果检测到的角点多于此数值,将返回最强的
maxCorners
个角点。如果maxCorners <= 0
,则返回检测到的所有角点。qualityLevel: 角点质量的特征值阈值。这个参数乘以图像中最佳角点的质量测量值(最小特征值或哈里斯函数响应),得到的乘积用于剔除质量较低的角点。
minDistance: 返回角点之间的最小可能欧几里得距离。这用于确保选取的角点之间有一定的空间分隔。
corners: 检测到的角点的输出向量。
mask: 感兴趣区域的掩码。如果提供,它必须是类型为
CV_8UC1
且大小与image
相同的二值图像。在掩码非零的区域内检测角点。blockSize: 计算导数协方差矩阵时考虑的邻域大小。
useHarrisDetector: 指示是否使用哈里斯角点检测器。如果不使用,则应用最小特征值方法(Shi-Tomasi方法)。
k: 仅当使用哈里斯检测器时使用的自由参数。
FAST(Features from Accelerated Segment Test)算法是一种广泛使用的角点检测方法。它以高速和简洁著称,在许多实时图像处理系统中发挥着重要作用。
- 选择像素点: 选择图像中的一个像素点P作为检测的中心点。
- 设置亮度阈值: 设定一个阈值T,用于与中心像素点P的亮度进行比较。
- 环形邻域检测: 在中心点P周围选择一个圆形邻域(通常包含16个像素点),用于判断P是否为角点。
- 连续性检查: 检查圆形邻域上是否有至少N个连续的像素点其亮度高于(或低于)P的亮度加上(或减去)阈值T。通常N设为12。
- 非极大值抑制: 应用非极大值抑制(Non-Maximum Suppression)技术,去除响应较弱的角点,只保留最显著的角点。
下面是一个基本的代码示例:
import cv2
# 读取图像
image = cv2.imread('tulips.jpg')
# 初始化FAST对象
fast = cv2.FastFeatureDetector_create(50)
# 检测角点
keypoints = fast.detect(image, None)
# 在图像上绘制角点
image_with_keypoints = cv2.drawKeypoints(image, keypoints, None, color=(0, 255, 0))
# 显示图像
cv2.imshow('FAST Keypoints', image_with_keypoints)
cv2.waitKey(0)
cv2.destroyAllWindows()
FastFeatureDetector_create
用于创建 FAST检测器的函数。
def FastFeatureDetector_create(threshold=None, nonmaxSuppression=None, type=None)
threshold: 用于决定角点的阈值。它是用来判断一个像素是否为角点的亮度或颜色强度变化的阈值。阈值越高,检测到的角点越少,但质量可能更高。
nonmaxSuppression: 非极大值抑制。当设置为
True
时,算法会在检测到的角点周围使用非极大值抑制,以确保检测到的角点是局部最大值,这样可以避免在角点附近检测到多个相邻的角点。通常,启用非极大值抑制会得到更好的结果。type: 指定 FAST 算法的类型。OpenCV 提供了不同版本的 FAST 算法,例如,TYPE_9_16、TYPE_7_12 等。这些类型指定了用于角点检测的圆形邻域的不同大小。
亚像素角点检测是一种提高角点定位精度到亚像素级别的技术。传统的角点检测方法,如哈里斯角点检测,通常只能定位到像素级别的精度。而在一些应用中,如高精度的图像对准和三维重建,需要更高精度的角点定位。亚像素角点检测通过对角点周围的像素进行精细分析,实现了这一目标。
亚像素角点检测的基本原理是利用图像局部区域的灰度分布信息来精细调整角点的位置。它通常包括以下几个步骤:
初步角点检测:首先使用常规方法(如哈里斯角点检测)在图像中定位角点的大致位置。
定义局部窗口:围绕每个初步检测到的角点,定义一个小的局部窗口。
灰度质心计算:在这个局部窗口中,计算灰度质心,这可以视为图像局部区域质量的“中心”。基于灰度质心的位置,可以对初步检测到的角点位置进行微调。
迭代优化:通过迭代的方式细化角点位置,直到满足一定的精度要求。
亚像素角点检测的关键公式是灰度质心的计算。灰度质心的坐标 ( x c , y c ) (x_c, y_c) (xc,yc) 可以用下面的公式计算:
x c = ∑ x i w i ∑ w i , y c = ∑ y i w i ∑ w i x_c = \frac{\sum x_i w_i}{\sum w_i}, \quad y_c = \frac{\sum y_i w_i}{\sum w_i} xc=∑wi∑xiwi,yc=∑wi∑yiwi
其中, x i , y i x_i, y_i xi,yi 是局部窗口中像素的坐标, w i w_i wi 是相应像素的灰度值。
import cv2
import numpy as np
# 读取图像
img = cv2.imread('tulips.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Harris角点检测
gray = np.float32(gray)
dst = cv2.cornerHarris(gray, blockSize=2, ksize=3, k=0.04)
# 结果阈值化,获取角点位置
dst = cv2.dilate(dst, None)
_, dst = cv2.threshold(dst, 0.01 * dst.max(), 255, 0)
dst = np.uint8(dst)
# 寻找质心并创建关键点集
_, _, _, centroids = cv2.connectedComponentsWithStats(dst)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.001)
corners = cv2.cornerSubPix(gray, np.float32(centroids), (5,5), (-1,-1), criteria)
keypoints = [cv2.KeyPoint(x=corner[0], y=corner[1], _size=20) for corner in corners]
# 使用drawKeypoints绘制关键点
img_keypoints = cv2.drawKeypoints(img, keypoints, None, color=(0,255,0))
# 显示图像
cv2.imshow('Harris Corners', img_keypoints)
cv2.waitKey(0)
cv2.destroyAllWindows()
这段代码的目的是在使用 Harris 角点检测后,找到检测到的角点的精确位置,并将它们转换为关键点对象,以便进一步处理或可视化。
1. cv2.connectedComponentsWithStats
ret, labels, stats, centroids = cv2.connectedComponentsWithStats(dst)
cv2.connectedComponentsWithStats
函数用于分析二值图像,并找出图像中所有连通区域(即相互连接的像素块)。dst
是 Harris 角点检测的结果,通常经过阈值处理以获得二值图像。ret
: 连通区域的数量。labels
: 图像大小的数组,其中的每个元素表示对应像素属于的连通区域的标签。stats
: 每个连通区域的统计信息,如区域的大小、边界框等。centroids
: 每个连通区域的质心(中心点)坐标。2. cv2.cornerSubPix
亚像素检测函数
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.001)
corners = cv2.cornerSubPix(gray, np.float32(centroids), (5,5), (-1,-1), criteria)
criteria
。这里使用了两个条件:迭代次数达到 100 或者角点位置变化小于 0.001。cv2.cornerSubPix
函数用于进一步精细化角点的位置。它基于初始的质心位置(即 centroids
),在一个小的邻域内寻找更精确的角点位置。gray
是原始的灰度图像,np.float32(centroids)
是上一步得到的质心坐标,(5,5)
是搜索窗口的大小,(-1,-1)
是死区的半径,通常不需要更改。3. 创建关键点对象
keypoints = [cv2.KeyPoint(x=corner[0], y=corner[1], _size=20) for corner in corners]
cv2.cornerSubPix
函数找到的角点坐标转换为 OpenCV 的 KeyPoint
对象。KeyPoint
对象包含了角点的位置和其他属性(如大小、方向等)。这里,我们只设置了位置(x
和 y
坐标)和大小(_size
),大小设为 20 仅为了可视化时更容易观察。特征点检测是指在图像中寻找具有独特属性的点,这些点在不同图像之间可以被匹配和识别。OpenCV提供了几种常用的特征点检测方法,如SIFT、SURF和ORB。
环境检查
运行本章代码时,请先检查Python和OpenCV的版本。
尤其在遇到报错:AttributeError: module 'cv2' has no attribute 'xfeatures2d_SIFT'
或者AttributeError: module 'cv2' has no attribute 'xfeatures2d'
的时候,参考下面的内容。
import sys
# 检查当前Python的版本
current_python_version = sys.version_info
# 检查Python版本是否是3.6
if current_python_version.major != 3 or current_python_version.minor != 6:
python_version_message = "当前Python版本不是3.6,建议安装Python 3.6版本。"
else:
python_version_message = "当前Python版本是3.6。满足运行环境。"
print(python_version_message)
import pkg_resources
# 设置所需检查的OpenCV版本
required_opencv_version = "3.4.2.16"
required_opencv_contrib_version = "3.4.2.16"
# 检查安装的OpenCV和opencv-contrib-python版本
try:
installed_opencv_version = pkg_resources.get_distribution("opencv-python").version
except pkg_resources.DistributionNotFound:
installed_opencv_version = None
try:
installed_opencv_contrib_version = pkg_resources.get_distribution("opencv-contrib-python").version
except pkg_resources.DistributionNotFound:
installed_opencv_contrib_version = None
# 构造安装提示信息
if installed_opencv_version != required_opencv_version or installed_opencv_contrib_version != required_opencv_contrib_version:
opencv_installation_message = """
# 卸载之前的OpenCV
pip uninstall opencv-python
# 安装指定版本的OpenCV和opencv-contrib-python
pip install opencv-python==3.4.2.16
pip install opencv-contrib-python==3.4.2.16
"""
else:
opencv_installation_message = "OpenCV和opencv-contrib-python版本正确。"
print(opencv_installation_message)
SIFT是一种用于检测和描述图像中的局部特征的算法。它在不同尺度的图像上查找关键点,并对每个关键点周围的区域进行特征描述。
SIFT算法的主要思想是在不同尺度空间上寻找关键点,并计算这些关键点的方向直方图作为特征。这些关键点对尺度和旋转具有不变性。
- 尺度空间极值检测:通过高斯差分函数在不同尺度空间查找潜在的兴趣点。
- 关键点定位:精确确定关键点的位置和尺度,去除低对比度的点和边缘响应点以增强匹配稳定性。
- 方向赋值:为每个关键点赋予一个或多个方向,基于局部图像梯度方向。
- 关键点描述:在每个关键点周围的区域内,计算其局部梯度的方向和幅度,生成描述符。
import cv2
img = cv2.imread('tulips.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
sift = cv2.xfeatures2d.SIFT_create()
keypoints, descriptors = sift.detectAndCompute(gray, None)
img = cv2.drawKeypoints(gray, keypoints, img,flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('SIFT Features', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
SIFT_create
SIFT_create
函数用于创建一个 SIFT 检测器对象。
def SIFT_create(nfeatures=None, nOctaveLayers=None, contrastThreshold=None, edgeThreshold=None, sigma=None)
nfeatures: 要保留的最佳特征数量。特征按其分数(在 SIFT 算法中作为局部对比度测量)排名。
nOctaveLayers: 每个八度中的层数。D. Lowe 的论文中使用的值是3。八度的数量是根据图像分辨率自动计算的。
contrastThreshold: 用于过滤掉半均匀(低对比度)区域中的弱特征的对比度阈值。阈值越大,检测器产生的特征越少。
edgeThreshold: 用于过滤掉边缘特征的阈值。注意其含义与 contrastThreshold 不同,即 edgeThreshold 越大,过滤掉的特征越少(保留的特征越多)。
sigma: 应用于第0个八度的输入图像的高斯模糊的 sigma 值。如果您的图像是用软镜头拍摄的低品质相机捕获的,可能需要减小这个值。
detectAndCompute
detectAndCompute
方法用于在图像中检测关键点并为它们计算描述符。
def detectAndCompute(self, image, mask, descriptors=None, useProvidedKeypoints=None)
image: 要处理的图像。
mask: 掩码图像,用于定义图像中要处理的区域。
descriptors: 计算得到的描述符将被存储在这里。
useProvidedKeypoints: 如果为 True,则该方法只计算指定关键点的描述符而不检测新的关键点。
这两个函数结合使用,可以在图像中检测并描述关键点,这些关键点及其描述符可用于后续的图像匹配和识别任务。
SURF是一种比SIFT更快的特征检测算法,同时保持了相似的特征描述能力。它对于快速和高效的图像匹配非常有用。
SURF算法改进了SIFT的计算效率。它使用积分图像快速计算高斯哈尔小波响应,并在多个尺度上寻找关键点。
- 尺度空间构建:利用积分图像提高了尺度空间构建的速度。
- 关键点检测:在不同尺度上使用盒子滤波器(近似高斯滤波器)查找关键点。
- 关键点描述:计算关键点周围的简化的Haar波特征描述符。
import cv2
img = cv2.imread('tulips.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
surf = cv2.xfeatures2d.SURF_create(1000)
keypoints, descriptors = surf.detectAndCompute(gray, None)
img = cv2.drawKeypoints(gray, keypoints, img,flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('SURF Features', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
SURF_create
函数用于创建一个 SURF 检测器对象。
def SURF_create(hessianThreshold=None, nOctaves=None, nOctaveLayers=None, extended=None, upright=None)
hessianThreshold: 用于 Hessian 关键点检测器的阈值。该值决定了关键点的选择——阈值越高,检测到的特征点越少但更显著。
nOctaves: 关键点检测器将使用的金字塔八度数。金字塔的每个八度包含图像的一个下采样版本。
nOctaveLayers: 每个八度内的层数。这影响到特征检测的尺度敏感性。
extended: 扩展描述符标志(true - 使用扩展的128元素描述符;false - 使用64元素描述符)。扩展描述符提供更多的特征信息,但也增加了计算复杂度。
upright: 是否计算特征的方向(true - 不计算特征的方向;false - 计算方向)。如果图像不会出现旋转变换,设置为 true 可以加快特征检测速度。
SURF可以在图像中检测并描述关键点,这些关键点及其描述符可用于图像匹配、对象识别等后续任务。SURF 由于其计算效率,经常用于实时或资源受限的应用场景。
ORB是一种结合了FAST关键点检测和BRIEF关键点描述的算法,以其速度和效率而著称。
ORB算法通过结合FAST算法的关键点检测和BRIEF算法的描述子,提供了一种快速且有效的特征点检测和描述方法。
import cv2
img = cv2.imread('tulips.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
orb = cv2.ORB_create(80)
keypoints, descriptors = orb.detectAndCompute(gray, None)
img = cv2.drawKeypoints(gray, keypoints, img, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('ORB Features', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
ORB_create
函数用于创建一个 ORB 检测器对象。
def ORB_create(nfeatures=None, scaleFactor=None, nlevels=None, edgeThreshold=None, firstLevel=None, WTA_K=None, scoreType=None, patchSize=None, fastThreshold=None)
nfeatures: 要保留的最大特征点数。
scaleFactor: 金字塔降采样比例,大于1。标准金字塔中scaleFactor=2,意味着每个级别的像素是前一个级别的1/4。
nlevels: 金字塔的层数。最小层的尺寸等于输入图像尺寸除以scaleFactor的nlevels - firstLevel次方。
edgeThreshold: 不检测特征的边缘大小,应大致匹配patchSize参数。
firstLevel: 将原始图像放置在金字塔的哪一层。前面的层由放大的原图填充。
WTA_K: 用于生成每个BRIEF描述符元素的点数。默认值为2,意味着对亮度进行简单的比较。
scoreType: 特征点评分类型。HARRIS_SCORE 使用 Harris 算法对特征点进行评分;FAST_SCORE 是另一种稍不稳定但计算速度更快的选项。
patchSize: 用于计算BRIEF描述符的补丁大小。
fastThreshold: FAST 算子的阈值。
ORB 的使用可以快速有效地在图像中检测和描述关键点,这些关键点及其描述符可用于图像匹配、对象识别等任务。ORB 算法因其高效性在移动和实时应用中特别受欢迎。
特征点匹配是在不同图像中找到相同特征点的过程。在OpenCV中,常用的方法包括BF匹配器(Brute-Force Matcher)和FLANN匹配器(Fast Library for Approximate Nearest Neighbors)。
BF匹配器是一种简单直接的匹配方法,主要通过计算一个特征描述子与其他所有描述子之间的距离,然后选择距离最小的匹配对。
- 距离计算:BF匹配器对一个特征集合中的每个特征描述子,计算它与另一个特征集合中所有特征描述子之间的距离。
- 最佳匹配选择:然后,选择距离最小的特征对作为匹配对。
- 距离度量:常用的距离度量包括欧氏距离、哈明距离等,具体使用哪种距离度量取决于描述子的类型。
import cv2
import numpy as np
# 读取图像
img1 = cv2.imread('tulips.jpg', 0)
img2 = cv2.imread('tulips_template.jpg', 0)
img2 = cv2.rotate(img2,cv2.ROTATE_90_COUNTERCLOCKWISE)
# 初始化SIFT检测器
sift = cv2.xfeatures2d_SIFT.create()
# 使用SIFT找到关键点和描述子
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# 创建BF匹配器对象
bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=True)
# 执行匹配
matches = bf.match(des1, des2)
# 绘制匹配
img3 = cv2.drawMatches(img1, kp1, img2, kp2, matches, None, flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
# 展示结果
cv2.imshow('BF Matcher', img3)
cv2.waitKey(0)
cv2.destroyAllWindows()
示例代码创建了一个 BFMatcher
对象,用 NORM_L2
作为距离度量,并启用了 crossCheck
。然后,使用 match
方法在两组描述符 des1
和 des2
之间找到最佳匹配。这种匹配方法特别适用于小型或中等大小的数据集,以及在需要高精度匹配时。
BFMatcher
BFMatcher
是一个暴力匹配器(Brute-Force Matcher),用于匹配不同图像中的描述符。它通过计算每个描述符之间的距离来查找匹配项。参数说明如下:
def create(cls, normType=None, crossCheck=None)
normType: 指定用于比较描述符的距离度量。常用的选项有
NORM_L1
,NORM_L2
,NORM_HAMMING
,NORM_HAMMING2
。NORM_L1
和NORM_L2
适合用于 SIFT 和 SURF
描述符,而NORM_HAMMING
适合于 ORB, BRISK 和 BRIEF。NORM_HAMMING2
适用于 ORB 的
WTA_K 为 3 或 4 时。crossCheck: 如果设置为
True
,则只返回互相匹配的描述符对(即对于每个查询描述符,在训练描述符集中找到最近的匹配,并且对于找到的匹配,在查询描述符集中也是最近的)。这通常会产生质量更高的匹配,但匹配数量可能较少。
match
match
方法用于查找两组描述符之间的最佳匹配。
def match(self, queryDescriptors, trainDescriptors, mask=None)
queryDescriptors: 查询描述符集合。
trainDescriptors: 训练描述符集合。这组描述符不会被添加到类对象存储的训练描述符集合中。
mask: 指定允许匹配的查询和训练描述符之间的掩码。如果查询描述符在掩码中被屏蔽,则此描述符不会添加匹配。因此,匹配的数量可能小于查询描述符的数量。
FLANN匹配器是一种更快的近似匹配方法,适用于大规模数据集。它使用优化的算法快速找到测试数据和训练集中的近似最近邻。
- 近似最近邻搜索:FLANN是基于多种优化算法(比如KD树、层次k均值树等)的集合,用于快速近似查找最近邻。
- 自动参数选择:FLANN能够根据数据自动选择最合适的算法和参数,优化搜索效率。
import cv2
import numpy as np
img1 = cv2.imread('tulips.jpg', 0)
img2 = cv2.imread('tulips_template.jpg', 0)
img2 = cv2.rotate(img2, cv2.ROTATE_90_COUNTERCLOCKWISE)
# 初始化SIFT检测器
sift = cv2.xfeatures2d_SIFT.create()
# 使用SIFT找到关键点和描述子
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# FLANN参数
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50)
# 创建FLANN匹配器对象
flann = cv2.FlannBasedMatcher(index_params, search_params)
# 执行匹配
matches = flann.knnMatch(des1, des2, k=2)
# 仅保留好的匹配
good_matches = []
for m, n in matches:
if m.distance < 0.7 * n.distance:
good_matches.append(m)
# 绘制匹配
img3 = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, flags=2)
# 展示结果
cv2.imshow('FLANN Matcher', img3)
cv2.waitKey(0)
cv2.destroyAllWindows()
示例代码创建了一个 FlannBasedMatcher
对象,使用 KDTREE 算法和相关参数。然后,使用 knnMatch
方法在两组描述符 des1
和 des2
之间找到每个描述符的 2 个最近邻(即 k=2)。这种方法特别适用于处理大型数据集,在寻找近似最近邻时可以获得更好的性能和效率。
FlannBasedMatcher
FlannBasedMatcher
是用于特征匹配的类。相比 BFMatcher
(暴力匹配器),它在处理大量数据时更加高效,尤其是当描述符数量很大时。参数说明如下:
index_params
: 字典格式的参数,用于指定索引参数。对于不同的算法,这些参数会有所不同。例如,当使用 KDTREE 算法时,可以设置trees
的数量。
search_params
: 字典格式的参数,用于指定搜索时的参数。例如,checks
参数控制着搜索时的迭代次数,影响搜索的准确性和效率。
knnMatch
knnMatch
方法用于找到每个查询描述符的 k 个最佳匹配项。参数如下:
queryDescriptors: 查询描述符集合。
trainDescriptors: 训练描述符集合。这组描述符不会被添加到类对象存储的训练描述符集合中。
k: 每个查询描述符要查找的最近邻个数。
mask: 一个掩码,指定允许的查询和训练描述符之间的匹配。
compactResult: 当掩码不为空时使用的参数。如果为
False
,则返回的匹配向量与查询描述符的行数相同。如果为True
,则不包含对于完全被掩码屏蔽的查询描述符的匹配。
RANSAC(随机抽样一致性算法)是一种鲁棒的特征点匹配算法,广泛用于处理含有大量噪声的数据。在特征点匹配过程中,RANSAC能有效地识别出正确的匹配点对(内点),并排除错误的匹配(外点)。
随机抽样:从所有匹配点对中随机选择一个小的子集来估计变换模型。例如,在计算两幅图像间的单应性(Homography)时,通常选择4对匹配点。
模型估计:使用这个子集来计算变换模型。例如,如果是计算单应性矩阵,这个步骤会产生一个单应性矩阵H。
内点计数:使用估计的模型测试所有的数据点,并计算模型一致的点的数量(内点)。这些点的集合称为一致性集。
模型验证:重复上述过程固定次数。每次重复后,如果一致性集的大小超过了之前的最大值,则更新最佳模型为当前模型。
最优模型:最后,使用内点集合来重新估计最优模型。
以下是使用OpenCV进行RANSAC特征点匹配的代码实例。
import cv2
import numpy as np
# 读取图像
img1 = cv2.imread('tulips.jpg', 0)
img2 = cv2.imread('tulips_template.jpg', 0)
img2 = cv2.rotate(img2, cv2.ROTATE_90_COUNTERCLOCKWISE)
# 初始化SIFT检测器
sift = cv2.xfeatures2d_SIFT.create()
# 使用SIFT找到关键点和描述子
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# 创建FLANN匹配器对象
flann = cv2.FlannBasedMatcher(dict(algorithm=1, trees=5), dict(checks=50))
matches = flann.knnMatch(des1, des2, k=2)
# 筛选好的匹配点
good_matches = []
for m, n in matches:
if m.distance < 0.7 * n.distance:
good_matches.append(m)
# 提取匹配点的位置
src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
# 使用RANSAC找到单应性矩阵
M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
matchesMask = mask.ravel().tolist()
# 绘制匹配结果
draw_params = dict(matchColor=(0, 255, 0), singlePointColor=None, matchesMask=matchesMask, flags=2)
img3 = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, **draw_params)
# 展示结果
cv2.imshow('RANSAC Matcher', img3)
cv2.waitKey(0)
cv2.destroyAllWindows()
在这段代码中,使用SIFT算法检测特征点,并通过FLANN匹配器进行匹配。然后,使用cv2.findHomography
函数结合RANSAC算法找到最佳单应性矩阵。其中, src_pts
和 dst_pts
是匹配点对,5.0
是 RANSAC 重投影误差的阈值。使用 RANSAC 方法可以有效处理异常值或外点,提高单应性矩阵估计的准确性。
findHomography
函数用于寻找两个平面之间的透视变换(单应性矩阵)的方法。这个函数常用于图像配准、3D重建和计算机视觉中的许多其他应用。以下是该函数的参数说明:
def findHomography(srcPoints, dstPoints, method=None, ransacReprojThreshold=None, mask=None, maxIters=None, confidence=None)
srcPoints: 原始平面中点的坐标。这可以是
CV_32FC2
类型的矩阵或vector
。dstPoints: 目标平面中点的坐标,格式与
srcPoints
相同。method:计算单应性矩阵的方法。可用的方法包括:
0
: 使用所有点的普通最小二乘法。cv2.RANSAC
: 基于 RANSAC 的鲁棒方法。cv2.LMEDS
: 最小中值鲁棒方法。cv2.RHO
: 基于 PROSAC 的鲁棒方法。ransacReprojThreshold(可选,仅在使用 RANSAC 或 RHO 时): 允许的最大重投影误差,用于将点对视为内点。在像素中测量时,这个值通常设置在 1 到 10 的范围内。
mask:由鲁棒方法(RANSAC 或 LMEDS)设置的可选输出掩码。掩码中的非零值表示内点。
maxIters:RANSAC 的最大迭代次数。
confidence:置信度水平,介于 0 和 1 之间。
函数返回值:
- retval: 单应性矩阵,如果无法估算则返回空矩阵。
- mask: 输出掩码,标识每个点对是内点还是外点。
在本博客中,我们详细探讨了OpenCV中用于特征点检测与匹配的多种方法。从基本的角点检测到复杂的特征点描述和匹配,这些方法在处理实际计算机视觉问题时发挥着至关重要的作用。通过对每种方法的原理、公式和代码实现的分析,我们可以更好地理解它们各自的优势和适用场景。