import cv2
import os
import time
import numpy as np
from glob import glob
from PIL import Image, ImageDraw, ImageFont
import math
# ------------------------------
# 配置参数 - 集中管理可配置项
# ------------------------------
class Config:
"""配置参数类,集中管理所有可调整的参数"""
# 路径配置
SAMPLES_DIR = r"E:\My Project\2409-Algorithm\python software\Test-photos\all"
TEST_IMAGE_PATH = r"E:\My Project\2409-Algorithm\python software\Test-photos\A08.jpg"
# ORB特征检测器参数
ORB_NFEATURES = 1000 # 最大特征点数量
ORB_EDGE_THRESHOLD = 31 # 边缘阈值,增大此值会减少边缘附近的特征点
ORB_SCALE_FACTOR = 1.2 # 金字塔缩放因子,接近1会生成更多特征点
ORB_NLEVELS = 8 # 金字塔层数
# 特征保留参数
KEEP_FEATURES = 500 # 保留的特征点数量
# 匹配参数
TOP_MATCHES = 100 # 保留的最佳匹配数量
RANSAC_THRESHOLD = 5.0 # RANSAC算法阈值
MIN_MATCHES_FOR_RANSAC = 10 # 应用RANSAC所需的最小匹配点数
# 缺陷检测参数
DIFF_THRESHOLD = 30 # 差分图像二值化阈值
MIN_DEFECT_AREA = 300 # 最小缺陷面积(像素)
MORPH_ITERATIONS = 2 # 形态学运算迭代次数
# 显示参数
DISPLAY_WIDTH = 600 # 中间结果显示宽度
MATCH_DISPLAY_WIDTH = 1200 # 匹配结果显示宽度
# ------------------------------
# 工具函数 - 图像处理辅助功能
# ------------------------------
def cv2_add_chinese_text(img, text, position, text_size=30, text_color=(255, 0, 0)):
"""
在OpenCV图像上添加中文文字
参数:
img: OpenCV图像(BGR格式)
text: 要添加的中文文字
position: 文字位置(x, y)
text_size: 文字大小
text_color: 文字颜色(BGR格式)
返回:
添加文字后的图像
"""
# 转换为PIL图像(RGB格式)
img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
# 创建绘制对象
draw = ImageDraw.Draw(img_pil)
# 加载支持中文的字体
font_paths = [
"C:/Windows/Fonts/simhei.ttf", # Windows系统黑体字体
"/System/Library/Fonts/PingFang.ttc", # macOS系统苹方字体
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" # Linux系统
]
# 尝试加载字体
font = None
for path in font_paths:
try:
if os.path.exists(path):
font = ImageFont.truetype(path, text_size, encoding="utf-8")
break
except IOError:
continue
# 如果所有指定字体都加载失败,使用默认字体
if font is None:
font = ImageFont.load_default()
print("警告:无法加载指定字体,使用默认字体可能导致中文显示异常")
# 绘制文字,注意颜色需要转换为RGB格式
rgb_color = (text_color[2], text_color[1], text_color[0])
draw.text(position, text, font=font, fill=rgb_color)
# 转换回OpenCV格式(BGR)
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
def prepare_image_for_display(img, title, width=Config.DISPLAY_WIDTH):
"""
调整图像大小并添加标题,用于中间结果显示
参数:
img: 输入图像
title: 要添加的标题
width: 调整后的宽度
返回:
处理后的图像
"""
# 调整大小
height, original_width = img.shape[:2]
scale = width / original_width
new_height = int(height * scale)
# 如果是单通道图像,转换为三通道以便添加彩色文字
if len(img.shape) == 2:
resized_img = cv2.cvtColor(cv2.resize(img, (width, new_height)), cv2.COLOR_GRAY2BGR)
else:
resized_img = cv2.resize(img, (width, new_height))
# 添加标题
return cv2_add_chinese_text(resized_img, title, (30, 30), 25, (0, 255, 0))
# ------------------------------
# 特征与变换处理函数
# ------------------------------
def extract_transform_params(homography_matrix):
"""
从单应矩阵中提取旋转角度(度)和平移量(dx, dy)
单应矩阵H描述了两个平面之间的映射关系,形式如下:
[h00 h01 h02]
[h10 h11 h12]
[h20 h21 h22]
我们可以从中提取出旋转、缩放和平移分量
参数:
homography_matrix: 3x3单应矩阵
返回:
(dx, dy, rotate_degrees): 平移量和旋转角度
"""
# 单应矩阵的前两行前三列包含了旋转、缩放和平移信息
H = homography_matrix[:2, :3]
# 提取旋转和缩放部分(前两列)
R = H[:, :2]
# 计算缩放因子(行列式的平方根)
s = np.sqrt(np.linalg.det(R))
# 归一化旋转矩阵(去除缩放影响)
R_normalized = R / s
# 计算旋转角度(弧度转换为度)
# 通过旋转矩阵元素计算角度: cosθ = R[0,0], sinθ = R[1,0]
rotate_radians = math.atan2(R_normalized[1, 0], R_normalized[0, 0])
rotate_degrees = math.degrees(rotate_radians)
# 提取平移分量(考虑缩放)
dx = H[0, 2] / s
dy = H[1, 2] / s
return dx, dy, rotate_degrees
def extract_image_features(img, orb_detector, max_keep=Config.KEEP_FEATURES):
"""
提取图像的ORB特征点和描述符
参数:
img: 输入图像(BGR格式)
orb_detector: ORB特征检测器
max_keep: 保留的最大特征点数量
返回:
(keypoints, descriptors): 特征点和描述符
"""
# 转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 检测特征点并计算描述符
kp, des = orb_detector.detectAndCompute(gray, None)
# 过滤并保留指定数量的特征点
if len(kp) > max_keep:
kp = kp[:max_keep]
des = des[:max_keep, :]
return kp, des
# ------------------------------
# 匹配与缺陷检测函数
# ------------------------------
def match_images(test_des, sample_des, matcher):
"""
匹配测试图像和样本图像的特征描述符
参数:
test_des: 测试图像的特征描述符
sample_des: 样本图像的特征描述符
matcher: 特征匹配器
返回:
(matches, match_time): 匹配结果和耗时(毫秒)
"""
start_time = time.time()
# 进行特征匹配
matches = matcher.match(test_des, sample_des)
# 按距离排序(距离越小匹配越好)
matches = sorted(matches, key=lambda x: x.distance)
# 计算耗时
match_time = (time.time() - start_time) * 1000 # 转换为毫秒
return matches, match_time
def detect_defects(test_img, sample_img, homography_matrix):
"""
检测测试图像与样本图像之间的差异(缺陷)
参数:
test_img: 测试图像
sample_img: 样本图像
homography_matrix: 单应矩阵(测试图像到样本图像的变换)
返回:
显示窗口名称列表
"""
# 获取图像尺寸
s_h, s_w = sample_img.shape[:2]
t_h, t_w = test_img.shape[:2]
# 计算单应矩阵的逆矩阵,将样本图像变换到测试图像的视角
# 这一步是为了使两张图像在同一坐标系下对齐,便于比较
h_inv = np.linalg.inv(homography_matrix)
aligned_sample = cv2.warpPerspective(sample_img, h_inv, (t_w, t_h))
# 转换为灰度图,便于计算差异
test_gray = cv2.cvtColor(test_img, cv2.COLOR_BGR2GRAY)
aligned_sample_gray = cv2.cvtColor(aligned_sample, cv2.COLOR_BGR2GRAY)
# 显示对齐结果对比
test_display = prepare_image_for_display(test_img, "原始测试图像(不变)")
aligned_sample_display = prepare_image_for_display(aligned_sample, "变换后的样本图像(对齐到测试图)")
aligned_combined = np.hstack((test_display, aligned_sample_display))
cv2.imshow("对齐结果对比", aligned_combined)
# 计算图像差分:原始测试图与变换后的样本图的差异
# 差异越大,表示该区域可能存在缺陷
diff = cv2.absdiff(test_gray, aligned_sample_gray)
# 应用固定阈值,将差异区域转换为二值图像
_, thresh = cv2.threshold(diff, Config.DIFF_THRESHOLD, 255, cv2.THRESH_BINARY)
# 创建结构元素,用于形态学处理
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
# 开运算:先腐蚀后膨胀,用于去除噪声和小面积区域
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=Config.MORPH_ITERATIONS)
# 查找所有轮廓
contours, _ = cv2.findContours(opening, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 过滤面积大于阈值的轮廓(认为是真实缺陷)
filtered_contours = [cnt for cnt in contours if cv2.contourArea(cnt) > Config.MIN_DEFECT_AREA]
print(f"检测到 {len(contours)} 个轮廓,过滤后保留 {len(filtered_contours)} 个面积大于{Config.MIN_DEFECT_AREA}的轮廓")
# 创建形态学处理图像的彩色副本,用于绘制所有缺陷
opening_color = cv2.cvtColor(opening, cv2.COLOR_GRAY2BGR)
# 线宽设置
contour_thickness = 3
# 在形态学处理图像上绘制所有缺陷
for cnt in contours:
cv2.drawContours(opening_color, [cnt], -1, (0, 0, 255), contour_thickness)
# 在原始测试图像上只绘制面积达标的缺陷
result_img = test_img.copy()
if len(filtered_contours) > 0:
for cnt in filtered_contours:
cv2.drawContours(result_img, [cnt], -1, (0, 0, 255), contour_thickness)
# 准备所有要显示的中间结果
display_images = [
(diff, "差分图像(原始测试图 - 变换后样本图)", "Difference Image"),
(thresh, f"二值化图像 (阈值={Config.DIFF_THRESHOLD})", "Thresholded Image"),
(opening, f"形态学处理后 (开运算x{Config.MORPH_ITERATIONS})", "After Morphology"),
(opening_color, "形态学图像上的所有缺陷", "All Defects on Morphology Image"),
(result_img, f"原始测试图上的缺陷检测结果 (> {Config.MIN_DEFECT_AREA}像素)", "Defects on Original Image")
]
# 显示所有中间结果图像
window_names = ["对齐结果对比"]
for img, chinese_title, english_title in display_images:
display_img = prepare_image_for_display(img, chinese_title)
cv2.imshow(english_title, display_img)
window_names.append(english_title)
return window_names
def show_matching_result(test_img, sample_path, test_kp, sample_kp, matches,
mask=None, method_name="", window_name="Matching Result"):
"""
显示图像匹配结果,包括特征点和匹配线
参数:
test_img: 测试图像
sample_path: 样本图像路径
test_kp: 测试图像特征点
sample_kp: 样本图像特征点
matches: 匹配结果
mask: 用于标记内点的掩码(RANSAC后)
method_name: 方法名称(用于标题)
window_name: 窗口名称
返回:
窗口名称
"""
# 读取样本图片
sample_img = cv2.imread(sample_path)
if sample_img is None:
print(f"无法读取样本图片: {sample_path}")
return None
# 复制原图用于标记特征点
test_img_marked = test_img.copy()
sample_img_marked = sample_img.copy()
# 标记特征点(用红色圆圈)
for kp in test_kp:
cv2.circle(test_img_marked, (int(kp.pt[0]), int(kp.pt[1])), 5, (0, 0, 255), 2)
for kp in sample_kp:
cv2.circle(sample_img_marked, (int(kp.pt[0]), int(kp.pt[1])), 5, (0, 0, 255), 2)
# 绘制匹配线
if mask is not None:
result_img = cv2.drawMatches(
test_img_marked, test_kp,
sample_img_marked, sample_kp,
matches, None,
matchColor=(0, 255, 0), # 内点匹配线为绿色
singlePointColor=(0, 0, 255), # 特征点为红色
matchesMask=mask,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
)
else:
result_img = cv2.drawMatches(
test_img_marked, test_kp,
sample_img_marked, sample_kp,
matches, None,
matchColor=(0, 255, 0), # 匹配线为绿色
singlePointColor=(0, 0, 255), # 特征点为红色
matchesMask=None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
)
# 调整图片大小,保持比例
height, width = result_img.shape[:2]
new_width = Config.MATCH_DISPLAY_WIDTH
new_height = int((new_width / width) * height)
resized_img = cv2.resize(result_img, (new_width, new_height))
# 添加标题
if method_name:
resized_img = cv2_add_chinese_text(resized_img, method_name, (50, 50), 30, (0, 0, 255))
# 显示图片
cv2.imshow(window_name, resized_img)
return window_name
# ------------------------------
# 主函数 - 程序入口
# ------------------------------
def main():
"""主函数,协调各个模块完成图像匹配和缺陷检测"""
# 1. 初始化与参数校验
print("===== 初始化 =====")
# 检查路径是否存在
if not os.path.exists(Config.SAMPLES_DIR):
print(f"错误:样本目录不存在 - {Config.SAMPLES_DIR}")
return
if not os.path.exists(Config.TEST_IMAGE_PATH):
print(f"错误:测试图片不存在 - {Config.TEST_IMAGE_PATH}")
return
# 获取所有样本图片路径
sample_paths = glob(os.path.join(Config.SAMPLES_DIR, "*.[jJ][pP][gG]")) + \
glob(os.path.join(Config.SAMPLES_DIR, "*.[pP][nN][gG]")) + \
glob(os.path.join(Config.SAMPLES_DIR, "*.[bB][mM][pP]"))
if not sample_paths:
print(f"错误:样本目录中没有找到图片 - {Config.SAMPLES_DIR}")
return
print(f"找到 {len(sample_paths)} 个样本图片")
print(f"测试图片路径:{Config.TEST_IMAGE_PATH}")
print("----------------------------------------")
# 2. 加载图像并提取特征
print("\n===== 特征提取 =====")
# 读取测试图片
test_img = cv2.imread(Config.TEST_IMAGE_PATH)
if test_img is None:
print(f"错误:无法读取测试图片 - {Config.TEST_IMAGE_PATH}")
return
test_img_gray = cv2.cvtColor(test_img, cv2.COLOR_BGR2GRAY)
t_h, t_w = test_img_gray.shape[:2]
print(f"测试图片尺寸:{t_w}x{t_h}")
# 初始化ORB特征检测器,使用配置参数
orb = cv2.ORB_create(
nfeatures=Config.ORB_NFEATURES,
edgeThreshold=Config.ORB_EDGE_THRESHOLD,
scaleFactor=Config.ORB_SCALE_FACTOR,
nlevels=Config.ORB_NLEVELS
)
# 提取测试图片特征
test_kp, test_des = extract_image_features(test_img, orb, Config.KEEP_FEATURES)
print(f"测试图片提取到 {len(test_kp)} 个ORB特征点")
# 预加载并提取所有样本图片的特征
print("正在加载样本图片并提取特征...")
sample_features = [] # (kp, des, width, height)
valid_samples = [] # 有效的样本路径
for path in sample_paths:
try:
img = cv2.imread(path)
if img is None:
continue
kp, des = extract_image_features(img, orb, Config.KEEP_FEATURES)
sample_features.append((kp, des, img.shape[1], img.shape[0]))
valid_samples.append(path)
except Exception as e:
print(f"加载样本 {path} 失败: {e}")
if not valid_samples:
print("错误:没有有效样本图片可用于匹配")
return
print(f"成功加载 {len(valid_samples)} 个有效样本图片")
print("----------------------------------------")
# 3. 图像匹配处理
print("\n===== 图像匹配 =====")
# 初始化BF匹配器
# 使用HAMMING距离,适用于ORB等二进制描述符
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
# 存储匹配结果:(路径, 匹配点数, 平均距离, 耗时, 匹配对象)
bf_results = []
bf_total_time = 0
print("开始比较图片...")
# 遍历所有样本进行匹配
for i, (sample_path, (sample_kp, sample_des, _, _)) in enumerate(zip(valid_samples, sample_features)):
# 进度提示
if (i + 1) % 10 == 0:
print(f"已处理 {i + 1}/{len(valid_samples)} 个样本")
# 进行特征匹配
matches, match_time = match_images(test_des, sample_des, bf)
bf_total_time += match_time
# 取前N个最佳匹配
top_matches = matches[:Config.TOP_MATCHES]
match_count = len(top_matches)
avg_distance = sum(m.distance for m in top_matches) / match_count if match_count > 0 else 0
bf_results.append((sample_path, match_count, avg_distance, match_time, top_matches))
print(f"样本 {i + 1}/{len(valid_samples)}: {os.path.basename(sample_path)} - "
f"匹配点数={match_count}, 平均距离={avg_distance:.2f}, 耗时={match_time:.2f}ms")
# 找出最佳匹配(排除匹配点数为0的样本)
bf_valid_results = [res for res in bf_results if res[1] > 0]
if not bf_valid_results:
print("警告:BF暴力匹配未找到任何有效匹配")
return
# 选择平均距离最小的作为最佳匹配
bf_best = min(bf_valid_results, key=lambda x: x[2])
# 4. 结果分析与可视化
print("\n===== 结果分析 =====")
# 获取最佳匹配的相关信息
bf_best_idx = bf_results.index(bf_best)
bf_best_matches = bf_results[bf_best_idx][4]
best_bf_sample_idx = valid_samples.index(bf_best[0])
best_bf_sample_kp, _, _, _ = sample_features[best_bf_sample_idx]
best_sample_img = cv2.imread(bf_best[0])
# 显示原始匹配结果
window_names = []
bf_window_name = "BF Matching - Original"
window_names.append(
show_matching_result(test_img, bf_best[0], test_kp, best_bf_sample_kp,
bf_best_matches, None, "原始匹配结果", bf_window_name))
# 应用RANSAC算法剔除异常匹配点
if len(bf_best_matches) >= Config.MIN_MATCHES_FOR_RANSAC:
# 提取匹配点的坐标
src_pts = np.float32([test_kp[m.queryIdx].pt for m in bf_best_matches]).reshape(-1, 1, 2)
dst_pts = np.float32([best_bf_sample_kp[m.trainIdx].pt for m in bf_best_matches]).reshape(-1, 1, 2)
# 使用RANSAC寻找单应矩阵并获取内点掩码
# 单应矩阵描述了两个平面之间的映射关系
homography_matrix, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, Config.RANSAC_THRESHOLD)
mask = mask.ravel().tolist()
# 提取变换参数
dx, dy, rotate = extract_transform_params(homography_matrix)
# 打印变换参数
print("\n变换参数:")
print(f"dx (水平平移): {dx:.2f} 像素")
print(f"dy (垂直平移): {dy:.2f} 像素")
print(f"rotate (旋转角度): {rotate:.2f} 度")
print(f"单应矩阵:\n{homography_matrix}")
# 统计内点数量
inliers_count = sum(mask)
print(f"\nRANSAC过滤结果: 原始匹配点 {len(bf_best_matches)}, "
f"保留内点 {inliers_count}, 剔除异常点 {len(bf_best_matches) - inliers_count}")
# 显示RANSAC过滤后的匹配结果
ransac_window_name = "BF Matching - After RANSAC"
window_names.append(
show_matching_result(test_img, bf_best[0], test_kp, best_bf_sample_kp,
bf_best_matches, mask, f"RANSAC过滤后 (保留{inliers_count}个匹配点)",
ransac_window_name))
# 显示缺陷检测结果及中间过程
defect_window_names = detect_defects(test_img, best_sample_img, homography_matrix)
window_names.extend(defect_window_names)
else:
print(f"\n匹配点数量不足({len(bf_best_matches)} < {Config.MIN_MATCHES_FOR_RANSAC}),无法应用RANSAC过滤")
# 短暂延迟确保窗口能显示出来
if window_names:
print("匹配结果窗口已显示...")
cv2.waitKey(1) # 非阻塞等待,确保窗口显示
# 输出匹配结果摘要
print("\n==================================================")
print("匹配结果摘要")
print("==================================================\n")
print("BF暴力匹配:")
print(f" 最相似样本: {os.path.basename(bf_best[0])}")
print(f" 最佳匹配点数: {bf_best[1]}")
print(f" 最佳平均距离: {bf_best[2]:.2f}(越小越好)")
print(f" 平均耗时: {bf_total_time / len(valid_samples):.2f} ms")
print(f" 总耗时: {bf_total_time:.2f} ms\n")
print("==================================================")
print("所有比较完成")
# 保持窗口显示,直到用户手动关闭
if window_names:
print("请查看匹配结果窗口,关闭窗口以结束程序...")
cv2.waitKey(0) # 等待用户关闭窗口
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
检查代码逻辑,合理性,并提出修改意见
最新发布