repo_description: PC自动玩尘白，自动化代理，尘白禁区助手，自动钓鱼，信源解析，水弹，异星守护，迷宫
repo_topics: automation fishing snowbreak
code: import json
  import os
  
  import cv2
  import numpy as np
  import pyautogui
  
  from app.common.config import config
  from app.common.image_utils import ImageUtils
  
  
  class Matcher:
      def __init__(self,
                   scale_steps=100,
                   scale_factor=0.01,
                   match_threshold=0.5,
                   early_stop_threshold=0.7,
                   nms_threshold=0.4):
          self.scale_steps = scale_steps
          self.scale_factor = scale_factor
          self.match_threshold = match_threshold
          self.early_stop_threshold = early_stop_threshold
          self.nms_threshold = nms_threshold
          self.max_scale = 1
          self.scales = {}
          self._init_scales()
          self._get_max_scale()
  
      def _init_scales(self):
          # 定义文件路径
          file_path = "AppData/scale_cache.json"
          if config.saveScaleCache.value:
              # 检查文件是否存在
              if os.path.exists(file_path):
                  try:
                      # 打开并读取 JSON 文件
                      with open(file_path, 'r', encoding='utf-8') as file:
                          self.scales = json.load(file)  # 将 JSON 数据加载到 self.scales
                      # print("scale_cache.json 文件已加载，数据已赋值给 self.scales。")
                  except Exception as e:
                      print(f"读取 scale_cache.json 文件时出错: {e}")
          else:
              if os.path.exists(file_path):
                  try:
                      # 删除文件
                      os.remove(file_path)
                      # print("scale_cache.json 文件已成功删除。")
                  except Exception as e:
                      print(f"删除 scale_cache.json 文件时出错: {e}")
  
      def save_scale_cache(self):
          # 定义文件路径
          file_path = "AppData/scale_cache.json"
  
          # 检查 AppData 目录是否存在，如果不存在则创建
          os.makedirs(os.path.dirname(file_path), exist_ok=True)
  
          try:
              # 将 self.scales 写入 JSON 文件
              with open(file_path, 'w', encoding='utf-8') as file:
                  json.dump(self.scales, file, indent=4)  # 使用 indent 参数格式化 JSON
              # print("self.scales 已成功保存到 scale_cache.json 文件。")
          except Exception as e:
              print(f"保存 scale_cache.json 文件时出错: {e}")
  
      def _get_max_scale(self):
          screen_width, screen_height = pyautogui.size()
          max_scale = max(1920 / screen_width, 1080 / screen_height) + 0.5
          self.max_scale = round(max_scale, 2)
          self.scale_steps = int(self.max_scale / 0.01)
  
      def step(self, scale, orig_w, orig_h, tpl_w, tpl_h, template, target, mask):
          step_boxes = []
          step_confidences = []
  
          resized_w = int(orig_w * scale)
          resized_h = int(orig_h * scale)
  
          # 跳过无效尺寸
          if resized_w < tpl_w or resized_h < tpl_h or scale <= 0:
              return [], []
  
          # 执行图像缩放
          resized = cv2.resize(target, (resized_w, resized_h))
  
          # 模板匹配（使用归一化相关系数法）
          result = cv2.matchTemplate(
              resized,
              template,
              cv2.TM_CCOEFF_NORMED,
              mask=mask)
  
          # 获取所有超过阈值的匹配位置
          loc = np.where(result >= self.match_threshold)
          for pt in zip(*loc[::-1]):  # pt为(x, y)坐标
              confidence = result[pt[1], pt[0]]
              if not np.isfinite(confidence):  # 跳过非有限值
                  continue
              # 将坐标转换回原始图像坐标系
              orig_x = int(pt[0] / scale)
              orig_y = int(pt[1] / scale)
              # 计算原始图像中的目标尺寸
              orig_tpl_w = int(tpl_w / scale)
              orig_tpl_h = int(tpl_h / scale)
              # 收集候选框（原始图像坐标系）
              step_boxes.append([
                  orig_x,
                  orig_y,
                  orig_tpl_w,
                  orig_tpl_h
              ])
              step_confidences.append(float(confidence))
          return step_confidences, step_boxes
  
      def match(self, template_path: str, target: cv2.typing.MatLike):
          template = cv2.imread(template_path, cv2.IMREAD_UNCHANGED)
          if template.shape[-1] == 4:
              mask = template[:, :, 3]  # 提取alpha通道
          else:
              mask = None
  
          # 预处理：锐化 + 边缘增强
          kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
          template = cv2.filter2D(template, -1, kernel)
          target = cv2.filter2D(target, -1, kernel)
          # 转换为灰度图
          template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
          target = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)
          # 获取模板尺寸
          tpl_h, tpl_w = template.shape[:2]
          orig_h, orig_w = target.shape[:2]
          # print(f"{template_path=},{template.shape=},{target.shape=}")
  
          if config.saveScaleCache.value:
              scale = self.scales.get(template_path, None)
          else:
              scale = None
          if scale is None:
              boxes = []
              confidences = [0]
              for i in range(self.scale_steps):
                  scale = self.max_scale - i * self.scale_factor
                  step_confidences, step_boxes = self.step(
                      scale,
                      orig_w,
                      orig_h,
                      tpl_w,
                      tpl_h,
                      template,
                      target,
                      mask
                  )
                  if len(step_confidences) == 0:
                      continue
                  if max(step_confidences) > max(confidences):
                      confidences = step_confidences
                      boxes = step_boxes
                      self.scales[template_path] = scale
                      # print(f"if的scale：{self.scales[template_path]}")
                      if min(confidences) > self.early_stop_threshold:
                          break
          else:
              # print(f'else的scale:{self.scales[template_path]}')
              confidences, boxes = self.step(
                  scale,
                  orig_w,
                  orig_h,
                  tpl_w,
                  tpl_h,
                  template,
                  target,
                  mask
              )
  
          if len(boxes) == 0:
              return []
  
          # 非极大值抑制（在原始图像坐标系进行）
          indices = cv2.dnn.NMSBoxes(
              boxes, confidences,
              score_threshold=self.match_threshold,
              nms_threshold=self.nms_threshold
          )
  
          # 收集检测框
          results = []
          if len(indices) > 0:
              for i in indices.flatten():
                  x, y, w, h = boxes[i]
                  confidence = confidences[i]
                  results.append((x, y, w, h, confidence))
          return results
  
  
  matcher = Matcher()
  # 使用示例
  # if __name__ == "__main__":
  #     from time import time
  #
  #     matcher = Matcher()
  #     orig_target = cv2.imread("C:/Users/laozhu/Desktop/test/PixPin_2025-02-28_16-05-00.png")
  #     start = time()
  #     matches = matcher.match("../resource/images/water_bomb/hp.png", orig_target)
  #     print(time() - start)
  #     print(matcher.scales)
  #     print(f"找到 {len(matches)} 个有效匹配：")
  #     for idx, (x, y, w, h, conf) in enumerate(matches):
  #         cv2.rectangle(orig_target,
  #                       (x, y),
  #                       (x + w, y + h),
  #                       (0, 255, 0), 2)
  #         text = f"{conf:.2f}"
  #         cv2.putText(orig_target, text,
  #                     (x, y - 5),
  #                     cv2.FONT_HERSHEY_SIMPLEX,
  #                     0.5, (0, 255, 0), 2)
  #     # 显示最终结果
  #     cv2.namedWindow("results")
  #     cv2.imshow("results", orig_target)
  #     cv2.waitKey(0)
  #     cv2.destroyAllWindows()
  