第四章:特征提取与描述——让机器“看懂”图像的关键

大家好,我是你们的老朋友。今天我们来聊聊计算机视觉里一个非常核心的话题——特征提取与描述。

你想想看,一张图片在计算机眼里是什么?其实就是一堆数字矩阵。但我们要让机器理解这张图里有什么,总不能把几百万个像素全扔给算法吧?那太笨了。我们需要找到图像里那些“有特点”的地方,然后用一种高效的方式描述它们。这就是特征提取与描述要做的事。

我个人习惯把特征分为两类:关键点特征区域特征。关键点特征关注的是图像中那些“与众不同”的点,比如角点、边缘交点;区域特征则关注一块区域的纹理、梯度分布等。今天我们要讲的Harris角点、SIFT/SURF、HOG、LBP,正好覆盖了这两大类。

核心思想:好的特征应该具备“可重复性”——换个角度、换个光照,同一个特征点还能被找到。说白了,就是特征要够“稳”。

特征提取与描述 关键点特征 区域/描述子特征 混合/梯度特征 Harris角点 SIFT / SURF LBP特征 HOG特征 尺度不变性 旋转不变性 图:本章特征提取与描述知识体系

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