这里是opencv的第4个模块,特征提取。

4.Feature Detection and Description

4.1简介

在这一章中,我们将尝试理解什么是"特征",为什么它们很重要,为什么角点(corners)是重要的特征等问题。

1.什么是特征

在计算机视觉中,"特征"是指图像中具有独特性的、易于识别的部分。比如:边缘(Edges)、角点(Corners,也称为兴趣点/关键点)、斑点(Blobs,也称为感兴趣区域)。

这些特征对于以下任务至关重要:

  • 图像匹配:找到两幅图像中的相同部分
  • 目标检测:识别图像中的特定物体
  • 跟踪:跟随视频中物体的运动
  • 3D重建:从多视角图像构建三维模型

2.为什么角点很重要

角点是两条边缘的交点,代表图像中两个方向变化最大的点。它具有旋转不变性(旋转图像后仍能检测到相同角点),这使得它比单纯边缘更稳定。

4.2Harris 角点检测

4.2.1Harris 角点检测原理

下图很好的解释了两个特征值和角点、边缘、平坦区域的关系,横轴为第一个特征值,纵轴为第二个特征值。

4.2.2cv.cornerHarris()、cv.cornerSubPix()、cv.dilate()

dst = cv.cornerHarris(src, blockSize, ksize, k)
src:输入单通道8位或浮点图像。
blockSize:邻域窗口大小(通常为2~5)。
ksize:Sobel算子的孔径(通常为3)。
k:Harris响应公式中的经验常数(通常0.04~0.06)。

返回值类型:是一个与输入图像同尺寸的 numpy.ndarray(浮点类型)。
cv.cornerSubPix()用于将角点位置精确到亚像素级(提高精度)。

参数 作用 推荐值 调整影响
blockSize 邻域窗口大小 2~5 值越大检测角点越少但越鲁棒
ksize Sobel梯度计算孔径 3 只能是1, 3, 5, 7
k 响应公式常数 0.04~0.06 值越小检测到的角点越多
dst = cv.dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]])
参数说明:
src:输入图像(通常为Harris检测后的响应矩阵或二值图)。
kernel:结构元素(决定膨胀的邻域形状)。
iterations:膨胀次数(默认为1)。
返回值:膨胀后的图像(与输入同尺寸)。

demo:提取图像角点

import cv2 as cv
import numpy as np

img = cv.imread('image4.png')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)

# Harris角点检测
gray = np.float32(gray)
dst = cv.cornerHarris(gray,blockSize=2,ksize=3,k=0.04)

# 标记角点
dst = cv.dilate(dst,None)
img[dst>0.01*dst.max()] = [0,255,0]

cv.imshow('Harris Corners',img)
cv.waitKey(0)
cv.destroyAllWindows()

不同阈值的情况:

显示dst:

4.3Shi-Tomasi Corner Detector-cv2.goodFeaturesToTrack

特征 Harris角点检测 Shi-Tomasi角点检测
评分函数 R = λ₁λ₂ - k(λ₁ + λ₂)² R = min(λ₁, λ₂)
参数敏感性 需要调优k值(通常0.04-0.06) 无需额外参数
计算复杂度 需要计算特征值分解 只需比较特征值大小
物理意义 衡量角点响应程度 直接反映最弱方向的边缘强度
  • λ₁ ≈ λ₂ ≈ 0:平坦区域

  • λ₁ >> λ₂ ≈ 0:边缘区域

  • λ₁ ≈ λ₂ >> 0:角点区域

corners = cv2.goodFeaturesToTrack(
    image,          # 输入图像(必须单通道!)
    maxCorners,     # 返回角点最大数量(≤0表示无限制)
    qualityLevel,   # 质量系数(0.01表示丢弃得分<最大得分1%的点)
    minDistance,    # 角点间最小欧氏距离(避免密集点)
    mask=None,      # 可选掩码(指定检测区域)
    blockSize=3,    # 计算协方差矩阵的邻域大小
    gradientSize=3, # Sobel算子孔径(默认3)
    useHarrisDetector=False,  # True=使用Harris算法
    k=0.04          # Harris算法自由参数
)

常见问题

问题现象 可能原因 解决方案
检测不到角点 qualityLevel设置过高 逐步降低至0.001尝试
角点集中在边缘 minDistance太小 增大到图像宽度的1/10
漏检真实角点 blockSize太小 增大到7/9(但不要超过15)
角点位置偏移 图像模糊 先做高斯模糊(σ=1)再检测

demo:提取角点

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt 

img = cv.imread('image.png')
img = cv.cvtColor(img,cv.COLOR_BGR2RGB)
gray = cv.cvtColor(img,cv.COLOR_RGB2GRAY)

corners = cv.goodFeaturesToTrack(gray,1000,0.01,10)
corners = np.int8(corners)

for i in corners:
    x,y = i.ravel()
    cv.circle(img,(x,y),3,255,-1)

plt.imshow(img),plt.show()

4.3SIFT (Scale-Invariant Feature Transform)-cv.SIFT_create()、sift.detect

在之前的几章中,我们学习了一些角点检测器,比如Harris角点检测等。它们是旋转不变的(rotation-invariant),也就是说,即使图像发生了旋转,我们仍然能够找到相同的角点。这很显然,因为在旋转后的图像中,角点仍然是角点。

但是,对于缩放(scaling)呢?如果图像被缩放,一个角点可能就不再是角点了。例如,请看下面这个简单的例子。在一个小图像中,一个小窗口内的角点,当图像被放大( zoomed in )后,在同一个窗口内它可能就变成了平坦的区域。因此,Harris角点不是尺度不变的(scale invariant)

2004年,不列颠哥伦比亚大学的D.Lowe在他的论文《来自尺度不变关键点的独特图像特征》中提出了一种新的算法,即尺度不变特征变换(SIFT),该算法可以提取关键点并计算其描述符。(本文易于理解,被认为是SIFT上最好的材料。本解释只是本文的简短总结)。

SIFT算法主要涉及四个步骤。我们将逐一看到他们。

1. Scale-space Extrema Detection

从上面的图像可以明显看出,我们无法使用相同的窗口来检测不同尺度的关键点。对于小的角点,这样处理没有问题。但要检测更大的角点,我们就需要更大的窗口。为此,我们采用了尺度空间滤波(scale-space filtering)的方法。

该方法的核心是使用不同的 σ 值计算图像的高斯拉普拉斯(Laplacian of Gaussian, LoG)。LoG 作为一种斑点检测器(blob detector),能通过改变 σ 值来检测不同大小的图像斑点。简而言之,σ 扮演了尺度参数(scaling parameter)的角色例如,在上图中,低 σ 值的高斯核会对小角点产生强响应,而高 σ 值的高斯核则更适合检测大角点。因此,我们可以在尺度(scale)和空间(space)两个维度上寻找局部极大值(local maxima),从而得到一系列包含 (σ) 值的点,这意味着在 σ 尺度下的 (x, y) 位置存在一个潜在的关键点。

但是,计算 LoG 的成本稍高,因此 SIFT 算法采用了高斯差分(Difference of Gaussians, DoG),这是对 LoG 的一种近似高斯差分是通过对同一图像分别用两个不同的 σ(设为 σ 和 kσ)进行高斯模糊,再将得到的两幅图像相减而得到的。这个过程会在高斯金字塔(Gaussian Pyramid)的不同组(octaves)中进行。下图形象地展示了这一过程:

在得到高斯差分(DoG)金字塔后,算法开始在尺度和空间两个维度上搜索图像的局部极值点。具体来说,图像中的每一个像素点都会与其同一尺度层的8个相邻像素,以及其上一尺度层和下一尺度层中对应位置的各9个像素(共9+9=18个)进行比较。

如果该像素值是这26个邻域像素中的最大值或最小值,那么它就被视为一个局部极值点,并作为一个潜在的关键点。这本质上意味着,在该尺度下,这个关键点的特征表现是最为突出和稳定的。下图展示了这一比较过程:

2. Keypoint Localization

在找到潜在的关键点位置后,需要对它们进行精确定位以获取更准确的结果。作者采用尺度空间的泰勒级数展开来获取极值点更精确的位置坐标,如果该极值点的响应强度低于某个阈值(根据论文设置为0.03),则会被剔除。这个阈值在OpenCV中被称为 contrastThreshold(对比度阈值)。

高斯差分(DoG)算子对边缘具有较高的响应,因此也需要消除边缘响应点。为此,采用了与哈里斯角点检测器相似的概念。算法使用一个2x2的海森矩阵(H)来计算主曲率。根据哈里斯角点检测的原理我们知道,在边缘处一个特征值会远大于另一个特征值。因此这里使用了一个简单的判别函数:

如果这个比值大于某个阈值(在OpenCV中称为 edgeThreshold),该关键点就会被丢弃。论文中给出的建议值为10。

通过以上步骤,算法成功剔除了低对比度的关键点边缘关键点,最终保留下来的就是具有显著特性的强兴趣点

3. Orientation Assignment

现在为每个关键点分配一个方向,以实现对图像旋转的不变性。根据关键点的尺度,在其周围取一个邻域,并在该区域内计算梯度的幅值和方向。创建一个包含36个柱子的方向直方图(覆盖360度),该直方图由梯度幅值和高斯加权的圆形窗口(其σ值为关键点尺度的1.5倍)进行加权。

取直方图中的最高峰,同时任何超过最高峰80%的峰值也会被用来计算方向。这会在相同位置和尺度上创建出具有不同方向的关键点。这种做法显著提高了匹配的稳定性。

4. Keypoint Descriptor

接下来是关键点描述子的创建。首先获取关键点周围 16x16 的邻域区域,然后将该区域划分为 16 个 4x4 大小的子块。针对每一个子块,计算其梯度方向并生成一个 8 柱的方向直方图。因此,最终总共会得到 128 个直方图柱的统计值(16个子块 * 8个方向)。这些值被组合成一个向量,就构成了该关键点的描述子。

除此之外,算法还采取了多种措施来确保描述子对光照变化旋转等因素具有鲁棒性。(例如:对描述子向量进行归一化以减弱光照影响,以及之前步骤中已实现的方向校正)。

5. Keypoint Matching

两幅图像之间的关键点通过识别最近邻的方式进行匹配。但在某些情况下,次优匹配最优匹配的距离可能非常接近。这可能是由于噪声或其他原因造成的。在这种情况下,需要计算最近距离与次近距离的比值。如果该比值大于 0.8,则拒绝此组匹配对。根据论文所述,这一步骤可以消除约 90% 的错误匹配,而仅舍弃约 5% 的正确匹配

6.SIFT in OpenCV

sift = cv.SIFT_create(nfeatures, nOctaveLayers, contrastThreshold, edgeThreshold, sigma)
nfeatures:保留的最佳特征数量(int类型,默认0保留所有)。
nOctaveLayers:金字塔每组层数(int类型,默认3)。
contrastThreshold:对比度阈值(double类型,默认0.04,值越小特征点越多)。
edgeThreshold:边缘阈值(double类型,默认10,值越大特征点越多)。
sigma:高斯滤波器初始标准差(double类型,默认1.6)。
返回值:是一个 cv2.SIFT 对象,用于后续的特征检测与描述。
keypoints = sift.detect(image, mask=None)
image:输入灰度图像。
mask:可选,指定检测区域的掩码。
返回值:是一个包含 cv2.KeyPoint 对象的列表。

sift.detect() 函数用于在图像中查找关键点。如果您只想搜索图像的一部分,可以传递一个掩码(mask)。每个关键点都是一个特殊结构,具有许多属性,例如其 (x, y) 坐标、有意义的邻域大小、指定其方向的角度、指定关键点强度的响应值等。

demo:

import numpy as np
import cv2 as cv

img = cv.imread('image4.png')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)

sift = cv.SIFT_create()
kp = sift.detect(gray,None)

img1 = cv.drawKeypoints(gray,kp,img)

cv.imwrite('sift_keypoint.jpg',img1)

OpenCV 还提供了 cv.drawKeyPoints() 函数,该函数在关键点位置绘制小圆圈。如果您向其传递标志 cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS,它将绘制一个与关键点大小相同的圆圈,甚至会显示其方向。参见以下示例。

4.4 SURF (Speeded-Up Robust Features)-建议使用ORB

在上一章中,我们了解了用于关键点检测与描述的SIFT算法。但其运算速度相对较慢,因此人们需要更高效的加速版本。2006年,Bay、H.、Tuytelaars、T. 与 Van Gool、L 三位学者联合发表论文《SURF:加速鲁棒特征》,提出了一种名为SURF的新算法。顾名思义,这是SIFT的加速演进版本。

在SIFT算法中,Lowe采用高斯差分来近似拉普拉斯高斯算子以实现尺度空间检测。而SURF则更进一步——通过盒式滤波器来逼近拉普拉斯高斯算子。下图直观展示了这种近似方法的实现过程。这种近似处理的最大优势在于:盒式滤波器的卷积运算可以借助积分图像快速完成,并且能够并行处理不同尺度的计算。此外,SURF算法依赖海森矩阵行列式同时实现尺度定位与空间定位

在方向分配环节,SURF算法采用尺寸为6s的邻域内水平与垂直方向的哈尔小波响应值,并施加相应的高斯权重系数。如图所示,这些响应值被映射到空间坐标系中,通过计算60度角滑动方向窗口内所有响应的总和来确定主方向。值得注意的是,利用积分图像可以极便捷地获取任意尺度下的小波响应。

针对许多无需旋转不变性的应用场景,算法无需进行方向计算从而显著提升速度——这种模式被称作直立SURF(U-SURF)。该模式在±15°视角范围内保持鲁棒性。OpenCV根据upright标志位支持两种模式:设为0时计算方向特征,设为1时启用快速直立模式。

简而言之,SURF增加了许多功能来提高每一步的速度。分析显示,它比SIFT快3倍,而性能可与SIFT相媲美。SURF擅长处理模糊和旋转的图像,但不擅长处理视角变化和照明变化。

surf = cv2.SURF_create(
    hessianThreshold=100,  # 海森矩阵阈值:控制特征点检测灵敏度
    nOctaves=4,           # 金字塔 octave 数量:影响尺度不变性
    nOctaveLayers=3,      # 每个octave中的层数
    extended=False,       # 描述符类型:False=64维,True=128维
    upright=False         # 方向敏感性:False=旋转不变,True=直立模式
)
参数详解:
hessianThreshold:建议范围 300-1000
值越小,检测到的特征点越多(包含更多噪声)
值越大,特征点越少但质量越高
nOctaves:建议范围 3-5
值越大,检测更大尺度变化的特征
计算复杂度随octave数量增加
nOctaveLayers:建议范围 2-4
每octave中尺度空间层数
extended:
False:64维描述符,计算更快
True:128维描述符,更具区分度
upright:
False:计算方向,具有旋转不变性
True:不计算方向,速度提升约20%
# SURF_create() 返回一个SURF特征检测器对象
surf = cv2.SURF_create()

# 该对象包含以下主要方法:
方法	返回类型	说明
detect(image, mask=None)	list[KeyPoint]	返回关键点列表
compute(image, keypoints)	tuple(list[KeyPoint], ndarray)	返回关键点和描述符
detectAndCompute(image, mask)	tuple(list[KeyPoint], ndarray)	同时检测和计算
getHessianThreshold()	float	获取当前海森矩阵阈值
setHessianThreshold(threshold)	None	设置海森矩阵阈值
getNOctaves()	int	获取金字塔组数
setNOctaves(octaves)	None	设置金字塔组数


# SURF特征检测与描述符计算
keypoints, descriptors = surf.detectAndCompute(
    image,          # 输入图像(灰度图)
    mask=None       # 可选参数:指定检测区域的掩码
)
keypoints:检测到的关键点列表,每个关键点包含:
KeyPoint(
    pt=(x, y),        # 关键点坐标
    size=20.0,        # 特征点直径
    angle=90.0,       # 方向角度(直立模式为-1)
    response=0.8,     # 海森矩阵响应强度
    octave=1,         # 金字塔组别
    class_id=-1       # 特征点类别
)
descriptors:描述符矩阵,形状为(n, 64)或(n, 128),数据类型为float32

demo:

surf的使用似乎需要更换package,opencv-python中没有这个方法

pip uninstall opencv-python
pip install opencv-contrib-python
特性 opencv-python opencv-contrib-python
包含内容 仅核心模块 核心模块 + contrib 模块
大小 较小 (~30MB) 较大 (~50MB)
功能 基础功能 完整功能(包括专利算法)
许可证 BSD 许可证 包含专利算法(使用时需注意)

然而我更换了版本依然无法使用,建议使用ORB。

参考

OpenCV: OpenCV-Python Tutorials

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐