第四章:特征提取与描述——让机器“看懂”图像的关键
大家好,我是你们的老朋友。今天我们来聊聊计算机视觉里一个非常核心的话题——特征提取与描述。
你想想看,一张图片在计算机眼里是什么?其实就是一堆数字矩阵。但我们要让机器理解这张图里有什么,总不能把几百万个像素全扔给算法吧?那太笨了。我们需要找到图像里那些“有特点”的地方,然后用一种高效的方式描述它们。这就是特征提取与描述要做的事。
我个人习惯把特征分为两类:关键点特征和区域特征。关键点特征关注的是图像中那些“与众不同”的点,比如角点、边缘交点;区域特征则关注一块区域的纹理、梯度分布等。今天我们要讲的Harris角点、SIFT/SURF、HOG、LBP,正好覆盖了这两大类。
核心思想:好的特征应该具备“可重复性”——换个角度、换个光照,同一个特征点还能被找到。说白了,就是特征要够“稳”。
4.1 Harris角点检测——最经典的角点探测器
先说说Harris角点。这玩意儿是1988年提出的,但到现在很多场景还在用。为什么?因为它简单、高效、够用。
角点是什么?就是图像里两个边缘交叉的地方。比如你拍一张棋盘格,那些黑白格子交叉的点就是角点。Harris的核心思想是:用一个窗口在图像上滑动,如果窗口往任何方向移动,窗口内的像素变化都很大,那这个窗口中心就是角点。
嗯,这里要注意。如果窗口在平坦区域,怎么移都没变化;如果在边缘上,沿着边缘方向移动变化小,垂直边缘方向变化大;只有在角点处,往哪个方向移变化都大。Harris就是用一个数学公式把这个“变化大小”量化了。
我的小经验:Harris角点对旋转和光照变化比较鲁棒,但对尺度变化很敏感。什么意思?你离得近拍到的角点,离远了可能就变成边缘了。所以如果你要做尺度不变的特征匹配,Harris就不太够用了。
代码实现其实很简单,OpenCV里一行就搞定:
import cv2
import numpy as np
img = cv2.imread('chessboard.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Harris角点检测
dst = cv2.cornerHarris(gray, blockSize=2, ksize=3, k=0.04)
# 标记角点
img[dst > 0.01 * dst.max()] = [0, 0, 255]
cv2.imshow('Harris Corners', img)
cv2.waitKey(0)
参数说明一下:blockSize是窗口大小,ksize是Sobel算子的孔径,k是Harris公式里的自由参数,一般取0.04-0.06。我在项目中遇到过一个问题——如果图像噪声大,Harris会检测出很多假角点。我的解决办法是先做高斯模糊,效果立竿见影。
4.2 SIFT与SURF——尺度不变性的王牌
说到SIFT,我得先感慨一句。这算法是2004年David Lowe提出的,到现在快20年了,依然是特征匹配领域的标杆。SIFT的全称是Scale-Invariant Feature Transform,翻译过来就是“尺度不变特征变换”。
它解决了Harris最大的痛点——尺度变化。你想想看,一张照片里的物体,近大远小,如果特征不能适应尺度变化,匹配就会出问题。SIFT通过构建高斯尺度空间,在不同尺度下检测极值点,然后精确定位关键点,再为每个关键点生成一个128维的描述子。
我曾经在一个项目里做无人机航拍图的拼接,用的就是SIFT。效果确实好,但速度嘛...说实话,有点慢。后来我换成了SURF,它是SIFT的加速版,用Hessian矩阵代替了DoG,用Haar小波代替了梯度直方图,速度提升了3-5倍。
注意:SIFT和SURF都是有专利的!SIFT的专利在2020年过期了,但SURF的专利还在。如果你做商业项目,要小心这一点。我个人建议,非商业研究可以用SIFT/SURF,商业项目可以考虑ORB或者AKAZE等开源替代方案。
来看一下SIFT的基本用法:
import cv2
img = cv2.imread('building.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 创建SIFT对象
sift = cv2.SIFT_create()
# 检测关键点和计算描述子
keypoints, descriptors = sift.detectAndCompute(gray, None)
# 绘制关键点
img_with_kp = cv2.drawKeypoints(img, keypoints, None,
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('SIFT Keypoints', img_with_kp)
cv2.waitKey(0)
你看,代码也不复杂。关键点用圆圈表示,圆圈大小代表尺度,方向代表主方向。SIFT的描述子是128维浮点数,匹配时一般用欧氏距离或者FLANN快速匹配。
4.3 HOG特征——行人检测的功臣
HOG,全称Histogram of Oriented Gradients,方向梯度直方图。这玩意儿在2005年被提出后,迅速成为行人检测的主流方法,直到后来被深度学习取代。
HOG的核心思想很简单:图像中物体的形状和外观,可以通过局部梯度方向的分布来刻画。说白了,就是统计图像局部区域里各个方向的梯度有多少。
具体做法是:先把图像分成小的cell(比如8x8像素),在每个cell里计算梯度方向和幅值,然后统计成梯度直方图。再把几个cell组成一个block,对block内的直方图做归一化,增强对光照变化的鲁棒性。最后把所有block的直方图串联起来,就是HOG特征向量。
我记得有一次做车辆检测,用HOG+SVM的组合,在高速公路场景下准确率能达到95%以上。但有个坑——HOG对遮挡很敏感,如果车被树挡住一半,特征就乱套了。
关键参数:
- cell大小:一般8x8或16x16,越小特征越精细,但维度越高
- block大小:一般2x2个cell,太大则丢失局部信息
- 梯度方向数:通常9个(0-180度,每20度一个bin)
- 归一化方式:L2-Hys(L2范数截断归一化)效果最好
OpenCV里提取HOG特征也很方便:
import cv2
import numpy as np
img = cv2.imread('pedestrian.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 创建HOG描述子
hog = cv2.HOGDescriptor()
# 计算HOG特征
features = hog.compute(gray)
print(f'HOG特征维度: {features.shape}')
# 使用预训练的行人检测器
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
rects, weights = hog.detectMultiScale(gray)
for (x, y, w, h) in rects:
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.imshow('HOG Pedestrian Detection', img)
cv2.waitKey(0)
4.4 LBP特征——纹理分析的利器
最后聊聊LBP,Local Binary Pattern,局部二值模式。这玩意儿特别适合做纹理分析,比如人脸识别、织物缺陷检测、医学图像分析等。
LBP的原理非常直观:对于图像中的每个像素,把它和周围的8个邻居像素比较。如果邻居像素值大于中心像素,就记为1,否则记为0。这样一圈比较下来,就得到一个8位的二进制数,转换成十进制就是该像素的LBP值。
你想想看,这其实是在编码局部纹理模式。比如平坦区域,所有邻居都和中心差不多,LBP值就是0或255;边缘区域,一半邻居比中心大,一半小,LBP值就有规律了。
我在做工业质检项目时用过LBP。当时要检测布料表面的瑕疵,用LBP提取纹理特征,配合简单的分类器,准确率就达到了98%。而且LBP计算速度极快,实时性完全没问题。
LBP有几个变种,我列个表对比一下:
| 变种名称 | 核心改进 | 适用场景 |
|---|---|---|
| 原始LBP | 3x3邻域,8个采样点 | 基础纹理分析 |
| 圆形LBP | 支持任意半径和采样点数 | 多尺度纹理分析 |
| 均匀LBP | 只保留跳变次数≤2的模式 | 降维、去噪 |
| 旋转不变LBP | 对LBP码循环移位取最小值 | 旋转不变的纹理识别 |
代码实现:
import cv2
import numpy as np
def lbp_original(img):
"""计算原始LBP特征"""
h, w = img.shape
lbp_img = np.zeros((h-2, w-2), dtype=np.uint8)
for i in range(1, h-1):
for j in range(1, w-1):
center = img[i, j]
code = 0
# 顺时针比较8个邻居
code |= (img[i-1, j-1] >= center) << 7
code |= (img[i-1, j] >= center) << 6
code |= (img[i-1, j+1] >= center) << 5
code |= (img[i, j+1] >= center) << 4
code |= (img[i+1, j+1] >= center) << 3
code |= (img[i+1, j] >= center) << 2
code |= (img[i+1, j-1] >= center) << 1
code |= (img[i, j-1] >= center) << 0
lbp_img[i-1, j-1] = code
# 计算LBP直方图作为特征向量
hist = cv2.calcHist([lbp_img], [0], None, [256], [0, 256])
hist = cv2.normalize(hist, hist).flatten()
return hist
# 使用OpenCV自带的LBP(更高效)
img = cv2.imread('fabric.jpg', 0)
# OpenCV的LBP人脸识别器内部使用了LBP特征
face_recognizer = cv2.face.LBPHFaceRecognizer_create()
# 实际使用时需要训练数据,这里只是演示API
我的建议:在实际项目中,不要只用一种特征。我经常把HOG和LBP结合起来——HOG抓形状,LBP抓纹理,互补效果很好。比如做行人检测时,HOG+LBP+SVM的组合,比单独用HOG能提升3-5个百分点的准确率。
好了,这一章的内容就到这里。特征提取是计算机视觉的基石,理解了这些经典方法,你再看深度学习里的卷积神经网络,会发现很多思想是相通的。比如CNN里的卷积层,本质上就是在学习各种特征提取器。嗯,这些经典方法虽然“老”,但它们的思路永远不会过时。
公众号:蓝海资料掘金营,微信deep3321