import cv2
import numpy as np
import matplotlib.pyplot as plt
import os
class FeatureMatcher:
"""
特征匹配与目标定位类
该类使用SIFT特征检测和FLANN匹配器实现目标定位功能,
支持尺度变化检测、结果可视化以及多种显示选项。
主要功能:
- 特征点检测与匹配
- 单应性矩阵计算
- 目标定位与尺度估计
- 结果可视化(带自定义信息显示)
- 多种布局选项
参数说明:
- max_matches: 最大匹配点数量
- min_match_count: 最小匹配点阈值
- show_lines: 是否显示匹配连线
- scale_range: 允许的尺度变化范围
- template_position: 模板在结果图像中的位置
- template_display_size: 模板显示尺寸
- font_size: 信息显示字体大小
"""
def __init__(self,
max_matches=150,
min_match_count=20,
show_lines=True,
scale_range=(0.7, 5.0),
template_position="top-right",
template_display_size=(400, 300),
font_size=0.9):
"""
初始化特征匹配器
参数:
max_matches: 最大匹配点数量 (默认: 150)
min_match_count: 最小匹配点阈值 (默认: 20)
show_lines: 是否显示匹配连线 (默认: True)
scale_range: 允许的尺度变化范围 (默认: (0.7, 5.0))
template_position: 模板在结果图像中的位置 (默认: 'top-right')
template_display_size: 模板显示尺寸 (width, height) (默认: (400, 300))
font_size: 信息显示字体大小 (默认: 0.9)
"""
# 配置参数
self.max_matches = max_matches
self.min_match_count = min_match_count
self.show_lines = show_lines
self.scale_range = scale_range
self.template_position = template_position
self.template_display_size = template_display_size
self.font_size = font_size
# 创建SIFT特征检测器
self.sift = cv2.SIFT_create()
# 创建FLANN匹配器
index_params = dict(algorithm=1, trees=5)
search_params = dict(checks=50)
self.flann = cv2.FlannBasedMatcher(index_params, search_params)
# 初始化结果变量
self.position = None
self.scale_factor = None
self.result_img = None
self.good_matches = []
self.kp_template = None
self.kp_target = None
def load_images(self, template_path, target_path):
"""
加载模板和目标图像
参数:
template_path: 模板图像路径
target_path: 目标图像路径
返回:
bool: 图像加载是否成功
"""
# 读取灰度图像
self.template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
self.target = cv2.imread(target_path, cv2.IMREAD_GRAYSCALE)
# 检查图像是否加载成功
if self.template is None or self.target is None:
print("错误:无法读取图像文件")
return False
# 获取目标图像原始尺寸
self.h_target_orig, self.w_target_orig = self.target.shape[:2]
self.target_filename = os.path.basename(target_path)
# 准备彩色结果图像
self.template_color = cv2.cvtColor(self.template, cv2.COLOR_GRAY2BGR)
self.target_color = cv2.cvtColor(self.target, cv2.COLOR_GRAY2BGR)
return True
def detect_features(self):
"""
检测图像特征点
使用SIFT算法检测模板和目标图像的特征点
"""
# 检测模板图像特征点
self.kp_template, self.des_template = self.sift.detectAndCompute(
self.template, None
)
print(f"模板检测到 {len(self.kp_template)} 个特征点")
# 检测目标图像特征点
self.kp_target, self.des_target = self.sift.detectAndCompute(
self.target, None
)
print(f"目标图像检测到 {len(self.kp_target)} 个特征点")
def match_features(self):
"""
匹配特征点并筛选优质匹配
使用FLANN匹配器进行特征匹配,并应用Lowe's比率测试筛选优质匹配点
"""
# 特征匹配
matches = self.flann.knnMatch(self.des_template, self.des_target, k=2)
# 应用Lowe's比率测试筛选优质匹配点
self.good_matches = []
for m, n in matches:
if m.distance < 0.7 * n.distance:
self.good_matches.append(m)
# 限制最大匹配点数量
if len(self.good_matches) > self.max_matches:
self.good_matches = sorted(
self.good_matches,
key=lambda x: x.distance
)[:self.max_matches]
print(f"筛选后有效匹配: {len(self.good_matches)}/{self.max_matches}")
def locate_target(self):
"""
定位目标并计算尺度因子
使用单应性矩阵计算目标位置和尺度变化
返回:
bool: 目标定位是否成功
"""
# 检查是否有足够的匹配点
if len(self.good_matches) < self.min_match_count:
print(f"匹配点不足! 需要至少 {self.min_match_count} 个匹配点")
return False
# 提取匹配点坐标
src_pts = np.float32(
[self.kp_template[m.queryIdx].pt for m in self.good_matches]
).reshape(-1, 1, 2)
dst_pts = np.float32(
[self.kp_target[m.trainIdx].pt for m in self.good_matches]
).reshape(-1, 1, 2)
# 计算单应性矩阵
H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
if H is None:
print("单应性矩阵计算失败")
return False
# 获取模板角点
h_template, w_template = self.template.shape
corners = np.float32([
[0, 0],
[0, h_template-1],
[w_template-1, h_template-1],
[w_template-1, 0]
]).reshape(-1, 1, 2)
# 变换角点
transformed_corners = cv2.perspectiveTransform(corners, H)
# 计算变换后的边界框尺寸
transformed_corners = transformed_corners[:, 0, :]
width = np.linalg.norm(transformed_corners[0] - transformed_corners[1])
height = np.linalg.norm(transformed_corners[1] - transformed_corners[2])
# 计算尺度因子
scale_w = width / w_template
scale_h = height / h_template
self.scale_factor = (scale_w + scale_h) / 2
# 检查尺度是否在允许范围内
min_scale, max_scale = self.scale_range
if self.scale_factor < min_scale or self.scale_factor > max_scale:
print(f"尺度变化超出范围: {self.scale_factor:.2f} (允许范围: {min_scale:.1f}-{max_scale:.1f})")
self.position = None
self.scale_factor = None
return False
self.target_color = cv2.polylines(
self.target_color, # 目标图像
[np.int32(transformed_corners.reshape(-1, 1, 2))], # 多边形顶点坐标
True, # 是否闭合多边形
(0, 255, 0), # 颜色 (BGR格式)
3, # 线宽
cv2.LINE_AA # 抗锯齿线型
)
# 计算中心点位置
self.position = np.mean(transformed_corners, axis=0)
pos_x, pos_y = int(self.position[0]), int(self.position[1])
# 绘制中心点
cv2.circle(self.target_color, (pos_x, pos_y), 10, (0, 255, 0), -1)
# 显示位置和尺度信息
cv2.putText(
self.target_color,
f"Position: ({pos_x}, {pos_y})",
(pos_x + 20, pos_y),
cv2.FONT_HERSHEY_SIMPLEX,
self.font_size,
(0, 255, 0),
2
)
cv2.putText(
self.target_color,
f"Scale: {self.scale_factor:.2f}x",
(pos_x + 20, pos_y + 40),
cv2.FONT_HERSHEY_SIMPLEX,
self.font_size,
(0, 255, 0),
2
)
return True
def draw_keypoints(self):
"""
在模板和目标图像上绘制特征点
"""
# 绘制模板图像特征点
cv2.drawKeypoints(
self.template_color,
self.kp_template,
self.template_color,
color=(0, 255, 0), # 绿色
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
)
# 绘制目标图像特征点
cv2.drawKeypoints(
self.target_color,
self.kp_target,
self.target_color,
color=(0, 255, 0), # 绿色
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
)
def create_result_image(self):
"""
创建结果图像
根据配置选项创建结果图像:
- 当 show_lines=True 时:显示匹配连线和模板
- 当 show_lines=False 时:只显示目标图像
"""
if self.show_lines:
# 处理模板显示尺寸
if self.template_display_size is not None:
# 提取指定的宽度和高度
display_width, display_height = self.template_display_size
# 计算缩放比例
scale_x = display_width / self.template_color.shape[1]
scale_y = display_height / self.template_color.shape[0]
# 缩放模板图像
template_display = cv2.resize(
self.template_color,
(display_width, display_height)
)
# 缩放模板特征点坐标
scaled_kp_template = []
for kp in self.kp_template:
# 创建新的关键点,坐标按比例缩放
x, y = kp.pt
new_kp = cv2.KeyPoint(
x * scale_x,
y * scale_y,
kp.size * max(scale_x, scale_y), # 根据缩放比例调整特征点大小
kp.angle,
kp.response,
kp.octave,
kp.class_id
)
scaled_kp_template.append(new_kp)
# 使用缩放后的特征点
kp_template_display = scaled_kp_template
h_template, w_template = template_display.shape[:2]
else:
# 使用原始特征点
template_display = self.template_color
kp_template_display = self.kp_template
h_template, w_template = template_display.shape[:2]
# 获取目标图像尺寸
h_target, w_target = self.target_color.shape[:2]
# 根据模板位置参数确定布局
if self.template_position == "top-right":
# 右上角布局
new_width = w_template + w_target
new_height = max(h_template, h_target)
# 创建新图像
self.result_img = np.zeros((new_height, new_width, 3), dtype=np.uint8)
# 放置目标图像在左侧
self.result_img[0:h_target, 0:w_target] = self.target_color
# 放置模板图像在右上角
start_x = new_width - w_template
self.result_img[0:h_template, start_x:start_x+w_template] = template_display
# 绘制匹配连线
for match in self.good_matches:
# 目标图像上的点
tar_pt = self.kp_target[match.trainIdx].pt
x1, y1 = int(tar_pt[0]), int(tar_pt[1])
# 模板图像上的点 (向右偏移目标图像宽度 + 模板在结果图像中的起始位置)
tmp_pt = kp_template_display[match.queryIdx].pt
x2 = int(tmp_pt[0]) + start_x
y2 = int(tmp_pt[1])
# 绘制连线
cv2.line(self.result_img, (x1, y1), (x2, y2), (0, 255, 0), 1)
# 在目标图像上绘制匹配点
cv2.circle(self.result_img, (x1, y1), 4, (255, 0, 0), -1)
# 在模板图像上绘制匹配点
cv2.circle(self.result_img, (x2, y2), 4, (255, 0, 0), -1)
# 添加分隔线
cv2.line(
self.result_img,
(w_target, 0),
(w_target, new_height),
(0, 0, 255), # 红色
2
)
else: # 默认左上角布局
self.result_img = cv2.drawMatches(
template_display,
kp_template_display,
self.target_color,
self.kp_target,
self.good_matches,
None,
matchColor=(0, 255, 0), # 绿色连线
singlePointColor=(255, 0, 0), # 蓝色为未匹配点
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
)
else:
# 仅显示目标图像(带定位框)
self.result_img = self.target_color.copy()
# 添加左上角信息
self.add_info_overlay()
# 添加右下角尺寸信息
self.add_size_info()
def add_info_overlay(self):
"""
在结果图像左上角添加信息覆盖层
显示以下信息:
1. 目标图像尺寸
2. 匹配点数量
3. 定位状态和尺度因子
4. 尺度范围
5. 模板位置和尺寸(当显示匹配连线时)
"""
# 计算行高和起始位置
line_height = int(40 * self.font_size)
y_start = 30
# 添加目标图像尺寸信息(第一行)
h_display, w_display = self.target_color.shape[:2]
cv2.putText(
self.result_img,
f"Background Size: {w_display}x{h_display}",
(10, y_start),
cv2.FONT_HERSHEY_SIMPLEX,
self.font_size,
(0, 255, 0), # 绿色
2
)
# 添加匹配信息(第二行)
y_start += line_height
cv2.putText(
self.result_img,
f"Matches: {len(self.good_matches)}/{self.max_matches}",
(10, y_start),
cv2.FONT_HERSHEY_SIMPLEX,
self.font_size,
(0, 255, 0),
2
)
# 添加定位状态和尺度信息(第三行)
y_start += line_height
if self.position is not None:
status_text = f"Located (Scale: {self.scale_factor:.2f}x)"
color = (0, 255, 0) # 绿色
else:
status_text = "Not Located"
color = (0, 0, 255) # 红色
cv2.putText(
self.result_img,
f"Status: {status_text}",
(10, y_start),
cv2.FONT_HERSHEY_SIMPLEX,
self.font_size,
color,
2
)
# 添加尺度范围信息(第四行)
y_start += line_height
cv2.putText(
self.result_img,
f"Scale Range: {self.scale_range[0]}-{self.scale_range[1]}",
(10, y_start),
cv2.FONT_HERSHEY_SIMPLEX,
self.font_size,
(0, 255, 0),
2
)
# 当显示匹配连线时,添加模板位置和尺寸信息
if self.show_lines:
# 添加模板位置信息(第五行)
y_start += line_height
cv2.putText(
self.result_img,
f"Template Position: {self.template_position}",
(10, y_start),
cv2.FONT_HERSHEY_SIMPLEX,
self.font_size,
(0, 255, 255), # 黄色
2
)
# 添加模板显示尺寸信息(第六行)
if self.template_display_size:
y_start += line_height
w, h = self.template_display_size
cv2.putText(
self.result_img,
f"Template Size: {w}x{h}",
(10, y_start),
cv2.FONT_HERSHEY_SIMPLEX,
self.font_size,
(0, 255, 255), # 黄色
2
)
def add_size_info(self):
"""
在结果图像右下角添加尺寸信息
"""
# 在右下角显示尺寸信息
text = f"{self.target_filename}: {self.w_target_orig}x{self.h_target_orig}"
(text_width, text_height), baseline = cv2.getTextSize(
text,
cv2.FONT_HERSHEY_SIMPLEX,
self.font_size,
2
)
# 计算文本位置
text_x = self.result_img.shape[1] - text_width - 10
text_y = self.result_img.shape[0] - 10
# 创建半透明背景
overlay = self.result_img.copy()
cv2.rectangle(
overlay,
(text_x - 5, text_y - text_height - 5),
(self.result_img.shape[1] - 5, text_y + 5),
(0, 0, 0),
-1
)
alpha = 0.6
self.result_img = cv2.addWeighted(overlay, alpha, self.result_img, 1 - alpha, 0)
# 绘制文本
cv2.putText(
self.result_img,
text,
(text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX,
self.font_size,
(0, 255, 0),
2
)
def match_and_locate(self, template_path, target_path):
"""
执行完整的特征匹配和目标定位流程
参数:
template_path: 模板图像路径
target_path: 目标图像路径
返回:
tuple: (定位成功状态, 位置坐标, 尺度因子, 结果图像)
"""
# 加载图像
if not self.load_images(template_path, target_path):
return False, None, None, None
# 检测特征点
self.detect_features()
# 匹配特征点
self.match_features()
# 尝试定位目标
located = self.locate_target()
# 绘制特征点
self.draw_keypoints()
# 创建结果图像
self.create_result_image()
return located, self.position, self.scale_factor, self.result_img
def display_result(self, save_path=None):
"""
显示并可选地保存结果图像
参数:
save_path: 结果图像保存路径(可选)
"""
if self.result_img is None:
print("错误:没有结果图像可显示")
return
# 创建Matplotlib图表
plt.figure(figsize=(12, 8))
plt.imshow(cv2.cvtColor(self.result_img, cv2.COLOR_BGR2RGB))
plt.axis('off')
# 添加标题
plt.title(f"Feature Matching - Target: {self.target_filename} ({self.w_target_orig}x{self.h_target_orig})")
# 显示图像
plt.show()
# 保存结果
if save_path:
cv2.imwrite(save_path, self.result_img)
print(f"结果图像已保存至: {save_path}")
# 输出定位信息
if self.position is not None:
print(f"目标定位成功! 位置: ({self.position[0]:.1f}, {self.position[1]:.1f}), 尺度: {self.scale_factor:.2f}x")
else:
print("目标定位失败")
# 使用示例
if __name__ == "__main__":
# 创建特征匹配器实例
matcher = FeatureMatcher(
max_matches=150,
min_match_count=20,
show_lines=True,
scale_range=(0.7, 5),
template_position="top-right",
template_display_size=(400, 300),
font_size=0.9
)
# 执行特征匹配和目标定位
located, position, scale_factor, result_img = matcher.match_and_locate(
template_path="gf124.png",
target_path="gf124.jpg"
)
# 显示结果
if result_img is not None:
# matcher.display_result(save_path="feature_matching_result.jpg")
matcher.display_result()
在这个基础上更加细分功能
最新发布