归一化互相关系数方法的工作原理
归一化的互相关系数(Normalized Cross-Correlation, NCC)是一种测量两个函数之间相似度的方法。对于图像处理而言,NCC 是通过比较模板图像和目标图像中的局部区域来评估它们之间的相似性的。
具体来说,对于每个模板可能的位置,NCC 计算如下:
\text{NCC}(x, y) = \frac{\sum_{i,j} (I(x+i, y+j) - \mu_I)(T(i, j) - \mu_T)}{\sqrt{\sum_{i,j}(I(x+i, y+j) - \mu_I)^2 \sum_{i,j}(T(i, j) - \mu_T)^2}}NCC(x,y)=∑i,j(I(x+i,y+j)−μI)2∑i,j(T(i,j)−μT)2∑i,j(I(x+i,y+j)−μI)(T(i,j)−μT)
其中 II 是源图像,TT 是模板图像,\mu_IμI 和 \mu_TμT 分别是源图像局部区域和模板图像的均值。
----
import cv2
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog
from PIL import Image, ImageTk
import numpy as np
import os
import glob
import datetime
FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"
class ImageVideoApp:
def __init__(self, master):
self.master = master
master.title("图像编辑器")
# 初始化变量
self.image_path = ""
self.template_path = ""
self.click_count = 0
self.points = []
self.rect_start = None
self.rect_end = None
self.scan_direction = "up_to_down"
self.selected_region = []
# 日志文件路径
self.log_file_path = r"D:\Picture_By\log.txt"
# 左侧图像显示区
self.left_frame = tk.Frame(master)
self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.left_image_label = tk.Label(self.left_frame)
self.left_image_label.pack()
# 右侧图像编辑区
self.right_frame = tk.Frame(master)
self.right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
self.right_image_label = tk.Label(self.right_frame)
self.right_image_label.pack()
# 加载图像按钮
self.load_image_button = tk.Button(master, text="加载图像", command=self.load_image)
self.load_image_button.pack(pady=10)
# 创建模板选择下拉菜单
self.template_frame = tk.Frame(master)
self.template_frame.pack(pady=10)
self.template_var = tk.StringVar()
self.template_var.set('请选择模板') # 设置默认值
self.template_menu = tk.OptionMenu(self.template_frame, self.template_var, '', command=self.load_template)
self.template_menu.pack()
# 文件名输入框
self.filename_entry = tk.Entry(master)
self.filename_entry.insert(0, "请输入模版文件名")
self.filename_entry.pack(pady=10)
# 编辑功能按钮
self.save_template_button = tk.Button(master, text="保存为模板", command=self.save_template)
self.save_template_button.pack(pady=10)
# 添加开始检测按钮
self.detect_button = tk.Button(master, text="开始检测", command=self.match_template)
self.detect_button.pack(pady=10)
# 匹配结果显示标签
self.match_result_label = tk.Label(master)
self.match_result_label.pack(side=tk.BOTTOM, pady=10)
# 绑定鼠标事件
self.left_image_label.bind("<Button-1>", self.on_click_start)
self.left_image_label.bind("<B1-Motion>", self.on_click_drag)
self.left_image_label.bind("<ButtonRelease-1>", self.on_click_release)
# 初始化模板列表
self.update_template_list()
def update_template_list(self):
self.template_options = []
for filename in glob.glob(r"D:\Picture_By\*_muban.*"):
self.template_options.append(os.path.basename(filename))
self.template_menu['menu'].delete(0, 'end')
for option in self.template_options:
menu = self.template_menu['menu']
menu.add_command(label=option, command=lambda fn=option: self.load_template(fn))
if self.template_options:
self.template_var.set(self.template_options[0]) # 设置第一个模板作为默认值
else:
self.template_var.set('请选择模板') # 如果没有模板,则设置为默认值
def log_event(self, message):
"""记录事件到日志文件"""
with open(self.log_file_path, 'a') as log_file:
log_file.write(f"{datetime.datetime.now()} - {message}\n")
def load_image(self):
self.image_path = filedialog.askopenfilename(title="选择图像文件", filetypes=[("图像文件", "*.jpg *.png *.bmp")])
if self.image_path:
self.display_image()
self.log_event("加载图像")
def display_image(self):
img = cv2.imread(self.image_path)
if img is None:
messagebox.showerror("错误", "无法加载图像,请检查路径是否正确!")
return
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_tk = ImageTk.PhotoImage(Image.fromarray(img_rgb))
self.left_image_label.config(image=img_tk)
self.left_image_label.image = img_tk
def load_template(self, path):
self.template_path = os.path.join("D:\\Picture_By", path)
img = cv2.imread(self.template_path)
if img is None:
messagebox.showerror("错误", "无法加载模板,请检查路径是否正确!")
return
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_tk = ImageTk.PhotoImage(Image.fromarray(img_rgb))
self.right_image_label.config(image=img_tk)
self.right_image_label.image = img_tk
self.log_event(f"加载模板: {path}")
def match_template(self):
if self.image_path and self.template_path:
self.log_event("开始检测")
image = cv2.imread(self.image_path)
template = cv2.imread(self.template_path)
res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
threshold = 0.8
# 使用 np.argmax 找到最大相关性位置
max_loc = np.argmax(res)
loc = np.unravel_index(max_loc, res.shape)
# 检查最大相关性是否超过阈值
if res[loc] >= threshold:
pt = (loc[1], loc[0]) # 注意:由于OpenCV坐标系统,这里需要交换顺序
# 计算模板在图像中的右下角坐标
bottom_right = (pt[0] + template.shape[1], pt[1] + template.shape[0])
# 绘制匹配区域的矩形框
cv2.rectangle(image, pt, bottom_right, (0, 0, 255), 2)
# 获取匹配区域的四个角点
corners = [
(pt[0], pt[1]),
(pt[0] + template.shape[1], pt[1]),
(pt[0] + template.shape[1], pt[1] + template.shape[0]),
(pt[0], pt[1] + template.shape[0])
]
# 使用最小外接矩形计算角度
rect = cv2.minAreaRect(np.array(corners))
width = int(rect[1][0])
height = int(rect[1][1])
angle = rect[2]
# 根据宽高调整角度
if width < height:
angle += 90
# 显示坐标和角度
cv2.putText(image, f"({pt[0]}, {pt[1]}, Angle: {angle:.2f})", (pt[0], pt[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
# 在右下角显示检测结果
self.draw_detection_result(image, "Finished Detection", fontPath=FONT_PATH)
img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
img_tk = ImageTk.PhotoImage(Image.fromarray(img_rgb))
self.right_image_label.config(image=img_tk, bg='blue')
self.right_image_label.image = img_tk
self.log_event("完成检测")
else:
messagebox.showinfo("Info", "No result found with higher than threshold.")
def draw_detection_result(self, img, text, fontPath=None, fontScale=1, thickness=2):
# 使用支持中文的字体文件
font = cv2.FONT_HERSHEY_SIMPLEX if fontPath is None else cv2.FONT_HERSHEY_SIMPLEX
font = cv2.FONT_HERSHEY_SIMPLEX if fontPath is None else cv2.QT_FONT_NORMAL
# 加载字体
font = cv2.QT_FONT_NORMAL if fontPath is None else cv2.imread(fontPath)
font = cv2.FONT_HERSHEY_SIMPLEX if fontPath is None else cv2.QT_FONT_NORMAL
font = cv2.FONT_HERSHEY_SIMPLEX if fontPath is None else cv2.QT_FONT_NORMAL
# 使用支持中文的字体文件
font = cv2.FONT_HERSHEY_SIMPLEX if fontPath is None else cv2.QT_FONT_NORMAL
font = cv2.QT_FONT_NORMAL if fontPath is None else cv2.FONT_HERSHEY_SIMPLEX
# 计算文字的位置(右下角)
(text_width, text_height), _ = cv2.getTextSize(text, font, fontScale, thickness)
img_height, img_width = img.shape[:2]
text_position = (img_width - text_width - 10, img_height - text_height - 10)
# 使用黑色字体
cv2.putText(img, text, text_position, font, fontScale, (0, 0, 0), thickness, lineType=cv2.LINE_AA)
def on_click_start(self, event):
self.rect_start = (event.x, event.y)
self.log_event("开始绘制矩形区域")
def on_click_drag(self, event):
self.rect_end = (event.x, event.y)
self.update_image_with_rectangle()
self.log_event("拖动矩形区域")
def on_click_release(self, event):
self.rect_end = (event.x, event.y)
self.update_image_with_rectangle()
self.move_selection_to_edit_area()
self.log_event("释放绘制矩形区域")
def update_image_with_rectangle(self):
if self.rect_start and self.rect_end:
img = cv2.imread(self.image_path)
if img is None:
messagebox.showerror("错误", "无法加载图像,请检查路径是否正确!")
return
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_tk = ImageTk.PhotoImage(Image.fromarray(img_rgb))
x1, y1 = min(self.rect_start[0], self.rect_end[0]), min(self.rect_start[1], self.rect_end[1])
x2, y2 = max(self.rect_start[0], self.rect_end[0]), max(self.rect_start[1], self.rect_end[1])
cv2.rectangle(img_rgb, (x1, y1), (x2, y2), (255, 0, 0), 2)
img_tk = ImageTk.PhotoImage(Image.fromarray(img_rgb))
self.left_image_label.config(image=img_tk)
self.left_image_label.image = img_tk
self.selected_region = [x1, y1, x2, y2]
def move_selection_to_edit_area(self):
if self.selected_region:
img = cv2.imread(self.image_path)
if img is None:
messagebox.showerror("错误", "无法加载图像,请检查路径是否正确!")
return
x1, y1, x2, y2 = self.selected_region
cropped_img = img[y1:y2, x1:x2]
cropped_img_rgb = cv2.cvtColor(cropped_img, cv2.COLOR_BGR2RGB)
cropped_img_tk = ImageTk.PhotoImage(Image.fromarray(cropped_img_rgb))
self.right_image_label.config(image=cropped_img_tk)
self.right_image_label.image = cropped_img_tk
self.log_event("移动选择到编辑区域")
def save_template(self):
filename = self.filename_entry.get().strip()
if not filename:
messagebox.showerror("错误", "请输入文件名!")
return
if self.right_image_label.image:
pil_image = ImageTk.getimage(self.right_image_label.image)
cropped_img_rgb = np.array(pil_image)
cropped_img_bgr = cv2.cvtColor(cropped_img_rgb, cv2.COLOR_RGB2BGR)
template_path = os.path.join("D:\\Picture_By", f"{filename}_muban.jpg")
try:
cv2.imwrite(template_path, cropped_img_bgr)
messagebox.showinfo("成功", f"模板已保存为 {template_path}!")
self.update_template_list()
self.log_event(f"保存模板: {filename}")
except UnicodeEncodeError:
messagebox.showerror("错误", "文件名包含无效字符,请检查文件名!")
self.log_event("保存模板失败: 文件名包含无效字符")
except Exception as e:
messagebox.showerror("错误", f"保存文件时发生错误:{str(e)}")
self.log_event(f"保存模板失败: {str(e)}")
def draw_line_and_circle(self, img):
if self.selected_region and len(self.selected_region) >= 2:
# 这里可以添加逻辑来绘制线条和圆圈
pass
def main():
root = tk.Tk()
app = ImageVideoApp(root)
root.mainloop()
if __name__ == "__main__":
main()