OpenCV 图像特征与匹配:SIFT 特征检测与 BFMatcher 暴力匹配
OpenCV 图像处理课程 · 第 8 章「Image Feature and Matcher」学习笔记
环境:Python 3.10 + opencv-python 4.5.5.64 + PyCharm(
chapter8目录)包含 2 课:SIFT 特征检测 / 特征匹配
章节总览
| 课 | 主题 | 核心函数 | 文件 |
|---|---|---|---|
| 01 | SIFT 特征检测 | cv2.SIFT_create() / sift.detectAndCompute() / cv2.drawKeypoints() |
sift_algo.py |
| 02 | 特征匹配 | cv2.BFMatcher() / bf.match() / cv2.drawMatches() |
feature_matching.py |
本章说明 :图像是一系列局部特征的集合(角点、边缘等)。特征检测用于识别图像中的关键点,特征匹配用于把两张图像里的相似特征配对。
第 1 课 · SIFT 特征检测
概念
- SIFT(Scale-Invariant Feature Transform,尺度不变特征变换)
- 开发于约 1999 年 ,不列颠哥伦比亚大学专利
- 商业应用需授权,但 2020-03-07 专利到期,现可免费商用
- 能检测图像中的局部特征(关键点),用于物体识别、跟踪、手势识别
- 在特征丰富的区域(如马的身体)检测到更多特征,背景等变化小的区域检测少
关键点 + 描述符
| 输出 | 含义 |
|---|---|
| keypoints | 特征位置(x,y 坐标),每个特征一个 KeyPoint 对象 |
| descriptors | 每个特征的128 维唯一向量,描述该特征 |
函数签名
python
sift = cv2.SIFT_create()
keypoints, descriptors = sift.detectAndCompute(gray_image, mask)
gray_image:输入灰度图mask:掩码(None= 整图检测;传二值图可限定区域)
python
image_features = cv2.drawKeypoints(image, keypoints, None, color)
color:关键点颜色(如(255,0,255)粉色)
完整代码(sift_algo.py)
python
import cv2
image_path = "../data/horse.jpg"
image = cv2.imread(image_path)
image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
sift = cv2.SIFT_create()
keypoints, descriptors = sift.detectAndCompute(image_gray, None)
print("length of keypoints:", len(keypoints)) # 约40个特征
print("descriptors shape:", descriptors.shape) # (N, 128)
image_features = cv2.drawKeypoints(image, keypoints, None, (255, 0, 255))
cv2.imshow("image features", image_features)
cv2.waitKey(0)
cv2.destroyAllWindows()
流程示意
#mermaid-svg-qlkztqsXFyjIvqY3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qlkztqsXFyjIvqY3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qlkztqsXFyjIvqY3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qlkztqsXFyjIvqY3 .error-icon{fill:#552222;}#mermaid-svg-qlkztqsXFyjIvqY3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qlkztqsXFyjIvqY3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qlkztqsXFyjIvqY3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qlkztqsXFyjIvqY3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qlkztqsXFyjIvqY3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qlkztqsXFyjIvqY3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qlkztqsXFyjIvqY3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qlkztqsXFyjIvqY3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qlkztqsXFyjIvqY3 .marker.cross{stroke:#333333;}#mermaid-svg-qlkztqsXFyjIvqY3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qlkztqsXFyjIvqY3 p{margin:0;}#mermaid-svg-qlkztqsXFyjIvqY3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qlkztqsXFyjIvqY3 .cluster-label text{fill:#333;}#mermaid-svg-qlkztqsXFyjIvqY3 .cluster-label span{color:#333;}#mermaid-svg-qlkztqsXFyjIvqY3 .cluster-label span p{background-color:transparent;}#mermaid-svg-qlkztqsXFyjIvqY3 .label text,#mermaid-svg-qlkztqsXFyjIvqY3 span{fill:#333;color:#333;}#mermaid-svg-qlkztqsXFyjIvqY3 .node rect,#mermaid-svg-qlkztqsXFyjIvqY3 .node circle,#mermaid-svg-qlkztqsXFyjIvqY3 .node ellipse,#mermaid-svg-qlkztqsXFyjIvqY3 .node polygon,#mermaid-svg-qlkztqsXFyjIvqY3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qlkztqsXFyjIvqY3 .rough-node .label text,#mermaid-svg-qlkztqsXFyjIvqY3 .node .label text,#mermaid-svg-qlkztqsXFyjIvqY3 .image-shape .label,#mermaid-svg-qlkztqsXFyjIvqY3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-qlkztqsXFyjIvqY3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qlkztqsXFyjIvqY3 .rough-node .label,#mermaid-svg-qlkztqsXFyjIvqY3 .node .label,#mermaid-svg-qlkztqsXFyjIvqY3 .image-shape .label,#mermaid-svg-qlkztqsXFyjIvqY3 .icon-shape .label{text-align:center;}#mermaid-svg-qlkztqsXFyjIvqY3 .node.clickable{cursor:pointer;}#mermaid-svg-qlkztqsXFyjIvqY3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qlkztqsXFyjIvqY3 .arrowheadPath{fill:#333333;}#mermaid-svg-qlkztqsXFyjIvqY3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qlkztqsXFyjIvqY3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qlkztqsXFyjIvqY3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qlkztqsXFyjIvqY3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qlkztqsXFyjIvqY3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qlkztqsXFyjIvqY3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qlkztqsXFyjIvqY3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qlkztqsXFyjIvqY3 .cluster text{fill:#333;}#mermaid-svg-qlkztqsXFyjIvqY3 .cluster span{color:#333;}#mermaid-svg-qlkztqsXFyjIvqY3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qlkztqsXFyjIvqY3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qlkztqsXFyjIvqY3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-qlkztqsXFyjIvqY3 .icon-shape,#mermaid-svg-qlkztqsXFyjIvqY3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qlkztqsXFyjIvqY3 .icon-shape p,#mermaid-svg-qlkztqsXFyjIvqY3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qlkztqsXFyjIvqY3 .icon-shape .label rect,#mermaid-svg-qlkztqsXFyjIvqY3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qlkztqsXFyjIvqY3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qlkztqsXFyjIvqY3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qlkztqsXFyjIvqY3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 特征
SIFT
转灰度
图像
BGR 彩色图
cvtColor 转灰度
SIFT_create()
detectAndCompute
keypoints(位置) +
descriptors(128维向量)
真实演示图(真实马头照片 + 粉色关键点圆圈):
💡 图片来源:Wikimedia Commons(CC BY-SA 4.0)------Chestnut French Trotter horse head
| SIFT 检测到的关键点 |
|---|
![]() |
关键点被标记在眼睛、耳朵、口鼻、辔头环等特征明显的区域(黄圈+红心)。
第 2 课 · 特征匹配(BFMatcher)
概念
- 利用 SIFT 得到的描述符 ,把两张图像里相似的特征配对
- 例:裁剪的人脸(图1)与完整场景(图2)之间的特征匹配
- 用暴力匹配(Brute-Force Matcher,BFMatcher) :比较两图描述符距离,距离越短匹配越好
函数签名
python
bf = cv2.BFMatcher()
matches = bf.match(descriptor1, descriptor2) # 按距离返回匹配
matches = sorted(matches, key=lambda x: x.distance) # 距离升序=最佳优先
image_match = cv2.drawMatches(img1, keypoints1, img2, keypoints2,
matches[:50], None)
bf.match(desc1, desc2):匹配两组描述符sorted(matches, key=lambda x: x.distance):按距离排序,取前 N 个最佳cv2.drawMatches(img1, kp1, img2, kp2, matches, None):把匹配连线画在两图间
完整代码(feature_matching.py)
python
import cv2
image_path1 = "../data/horse_face.jpg"
image_path2 = "../data/horse.jpg"
image1 = cv2.imread(image_path1)
image2 = cv2.imread(image_path2)
image_gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
image_gray2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
sift = cv2.SIFT_create()
keypoints1, descriptor1 = sift.detectAndCompute(image_gray1, None)
keypoints2, descriptor2 = sift.detectAndCompute(image_gray2, None)
bf = cv2.BFMatcher()
matches = bf.match(descriptor1, descriptor2)
matches = sorted(matches, key=lambda x: x.distance) # 最佳优先
image_match = cv2.drawMatches(image1, keypoints1,
image2, keypoints2, matches[:50], None)
cv2.imshow("image match", image_match)
cv2.waitKey(0)
cv2.destroyAllWindows()
流程示意
#mermaid-svg-r50u5rCK6MNevMeb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-r50u5rCK6MNevMeb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-r50u5rCK6MNevMeb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-r50u5rCK6MNevMeb .error-icon{fill:#552222;}#mermaid-svg-r50u5rCK6MNevMeb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-r50u5rCK6MNevMeb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-r50u5rCK6MNevMeb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-r50u5rCK6MNevMeb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-r50u5rCK6MNevMeb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-r50u5rCK6MNevMeb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-r50u5rCK6MNevMeb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-r50u5rCK6MNevMeb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-r50u5rCK6MNevMeb .marker.cross{stroke:#333333;}#mermaid-svg-r50u5rCK6MNevMeb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-r50u5rCK6MNevMeb p{margin:0;}#mermaid-svg-r50u5rCK6MNevMeb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-r50u5rCK6MNevMeb .cluster-label text{fill:#333;}#mermaid-svg-r50u5rCK6MNevMeb .cluster-label span{color:#333;}#mermaid-svg-r50u5rCK6MNevMeb .cluster-label span p{background-color:transparent;}#mermaid-svg-r50u5rCK6MNevMeb .label text,#mermaid-svg-r50u5rCK6MNevMeb span{fill:#333;color:#333;}#mermaid-svg-r50u5rCK6MNevMeb .node rect,#mermaid-svg-r50u5rCK6MNevMeb .node circle,#mermaid-svg-r50u5rCK6MNevMeb .node ellipse,#mermaid-svg-r50u5rCK6MNevMeb .node polygon,#mermaid-svg-r50u5rCK6MNevMeb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-r50u5rCK6MNevMeb .rough-node .label text,#mermaid-svg-r50u5rCK6MNevMeb .node .label text,#mermaid-svg-r50u5rCK6MNevMeb .image-shape .label,#mermaid-svg-r50u5rCK6MNevMeb .icon-shape .label{text-anchor:middle;}#mermaid-svg-r50u5rCK6MNevMeb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-r50u5rCK6MNevMeb .rough-node .label,#mermaid-svg-r50u5rCK6MNevMeb .node .label,#mermaid-svg-r50u5rCK6MNevMeb .image-shape .label,#mermaid-svg-r50u5rCK6MNevMeb .icon-shape .label{text-align:center;}#mermaid-svg-r50u5rCK6MNevMeb .node.clickable{cursor:pointer;}#mermaid-svg-r50u5rCK6MNevMeb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-r50u5rCK6MNevMeb .arrowheadPath{fill:#333333;}#mermaid-svg-r50u5rCK6MNevMeb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-r50u5rCK6MNevMeb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-r50u5rCK6MNevMeb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-r50u5rCK6MNevMeb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-r50u5rCK6MNevMeb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-r50u5rCK6MNevMeb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-r50u5rCK6MNevMeb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-r50u5rCK6MNevMeb .cluster text{fill:#333;}#mermaid-svg-r50u5rCK6MNevMeb .cluster span{color:#333;}#mermaid-svg-r50u5rCK6MNevMeb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-r50u5rCK6MNevMeb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-r50u5rCK6MNevMeb rect.text{fill:none;stroke-width:0;}#mermaid-svg-r50u5rCK6MNevMeb .icon-shape,#mermaid-svg-r50u5rCK6MNevMeb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-r50u5rCK6MNevMeb .icon-shape p,#mermaid-svg-r50u5rCK6MNevMeb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-r50u5rCK6MNevMeb .icon-shape .label rect,#mermaid-svg-r50u5rCK6MNevMeb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-r50u5rCK6MNevMeb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-r50u5rCK6MNevMeb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-r50u5rCK6MNevMeb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} drawMatches
BFMatcher
SIFT 各自检测
两张图
图1 (裁剪)
horse_face
图2 (整体)
horse
kp1 + desc1
kp2 + desc2
bf.match(desc1, desc2)
按距离排序,取前50
两图间连线相同特征
真实演示图(左=完整马头,右=马脸裁剪放大,绿色连线=匹配特征):
💡 图片来源:Wikimedia Commons(CC BY-SA 4.0)
| 特征匹配结果(局部裁剪 ↔ 整体) |
|---|
![]() |
左侧绿色框圈出马脸区域,右侧为该区域放大图;每条绿线把框内特征点(眼睛、口鼻、辔头环)连到放大图对应位置,证明局部特征能与整体匹配。
本章小结(速查表)
| 操作 | 函数 | 关键点 |
|---|---|---|
| 建 SIFT | cv2.SIFT_create() |
创建 SIFT 对象 |
| 检测特征 | sift.detectAndCompute(img, mask) |
返回(keypoints, descriptors),desc 128维 |
| 画关键点 | cv2.drawKeypoints(img, kp, None, color) |
图上标记关键点 |
| 建匹配器 | cv2.BFMatcher() |
暴力匹配 |
| 匹配 | bf.match(desc1, desc2) |
按距离返回匹配 |
| 排序 | sorted(matches, key=lambda x: x.distance) |
距离升序=最佳优先 |
| 画匹配 | cv2.drawMatches(img1, kp1, img2, kp2, matches, None) |
两图连线 |
核心易错点
- SIFT 输入需灰度图 (BGR 先
cvtColor) detectAndCompute返回 两个值 :(keypoints, descriptors)descriptors.shape是(N, 128)------每个特征 128 维- 匹配排序 用
key=lambda x: x.distance,距离越短越好 drawMatches取前 N 个(如matches[:50])画最佳匹配

