第11讲:2.指数平滑模型

本文介绍了指数平滑法的不同模型,包括简单指数平滑、线性趋势模型、布朗线性趋势模型、阻尼趋势模型及季节性模型。讨论了平滑系数的选择原则,并对比了不同模型的应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

spss软件会帮助我们选择使用哪种模型,在论文中根据模型解释参数和公式

简单指数平滑法(simple模型)

推导:

关于平滑系数𝛼的选取原则:
1、如果时间序列具有不规则的起伏变化,但长期趋势接近一个稳定常数,α值一
般较小(0.05‐0.02之间
2、如果时间序列具有迅速明显的变化倾向,则α应该取较大值(取0.3‐0.5
3、如果时间序列变化缓慢,亦应选较小的值(一般在0.1‐0.4之间)
实际上,Spss的专家建模如果选择了Simple模型用来估计,那么软件会帮我们自动
选取一个适合的平滑系数使得预测误差最小。
简单指数平滑法预测,只能预测一期,这是由我们的公式所决定的。

线性趋势模型(linear trend)

Holt 在1957年把简单的指数平滑模型进行了延伸,能够预测包含趋势的数据,
该方法包含一个预测方程和两个平滑方程(一个用于水平,另一个用于趋势):

布朗(Brown)线性趋势模型
假定𝛼=𝛽,即认为水平平滑参 数和趋势平滑参数相等。 (是Holt线性趋势模型的特例)

阻尼趋势模型(Damped trend

 经验表明,Holt的线性趋势模型倾向于对未来预测值过高,特别是对于长期预测。Gardner McKenzie (1985)在霍特的模型基础上引入了一种阻尼效应, 用来缓解较高的线性趋势。

\varphi越小说明阻尼越大,越接近于1阻力越小

 霍特趋势预测和阻尼趋势预测:

 增长率逐渐下降,并没有保持一个持续增长的线性时,阻尼趋势更加合适

简单季节性(Simple seasonal

k=[...](取整符号)

温特加法模型(Winters' additive

 温特乘法模型(Winters' multiplicative

温特加法与温特乘法预测:

季节成分是否稳定,用加发还是乘法,言之有理即可

import tkinter as tk import cv2 import time import torch from ultralytics import YOLO from PIL import Image, ImageTk import threading import queue import dxcam import traceback import ctypes from tkinter import ttk, messagebox import os import glob import sys import logitech.lg from pynput import keyboard class PIDController: """PID控制器""" def __init__(self, kp=0.0, ki=0.0, kd=0.0): self.kp = kp self.ki = ki self.kd = kd self.prev_error = (0, 0) self.integral = (0, 0) self.last_time = time.time() def update(self, error): current_time = time.time() dt = current_time - self.last_time self.last_time = current_time if dt <= 0: dt = 0.01 dx, dy = error px = self.kp * dx py = self.kp * dy self.integral = ( self.integral[0] + dx * dt, self.integral[1] + dy * dt ) ix = self.ki * self.integral[0] iy = self.ki * self.integral[1] dx_dt = (dx - self.prev_error[0]) / dt dy_dt = (dy - self.prev_error[1]) / dt ddx = self.kd * dx_dt ddy = self.kd * dy_dt self.prev_error = (dx, dy) output_x = px + ix + ddx output_y = py + iy + ddy return (output_x, output_y) def reset(self): self.prev_error = (0, 0) self.integral = (0, 0) self.last_time = time.time() class ScreenDetector: def __init__(self, config_path): # 解析配置文件 self._parse_config(config_path) # 设备检测与模型加载 self.device = self._determine_device() self.model = YOLO(self.model_path).to(self.device) # 屏幕信息初始化 self._init_screen_info() # 控制参数初始化 self._init_control_params() # 状态管理 self.stop_event = threading.Event() self.camera_lock = threading.Lock() self.target_lock = threading.Lock() self.offset_lock = threading.Lock() self.shift_lock = threading.Lock() # 初始化相机 self._init_camera() # 初始化键盘监听 self._init_keyboard_listener() def _parse_config(self, config_path): """解析并存储配置参数""" self.cfg = self._parse_txt_config(config_path) # 存储常用参数 self.model_path = self.cfg['model_path'] self.model_device = self.cfg['model_device'] self.screen_target_size = int(self.cfg['screen_target_size']) self.detection_conf_thres = float(self.cfg['detection_conf_thres']) self.detection_iou_thres = float(self.cfg['detection_iou_thres']) self.detection_classes = [int(x) for x in self.cfg['detection_classes'].split(',')] self.visualization_color = tuple(map(int, self.cfg['visualization_color'].split(','))) self.visualization_line_width = int(self.cfg['visualization_line_width']) self.visualization_font_scale = float(self.cfg['visualization_font_scale']) self.visualization_show_conf = bool(self.cfg['visualization_show_conf']) self.fov_horizontal = float(self.cfg.get('move_fov_horizontal', '90')) self.mouse_dpi = int(self.cfg.get('move_mouse_dpi', '400')) self.pid_kp = float(self.cfg.get('pid_kp', '0.5')) self.pid_ki = float(self.cfg.get('pid_ki', '0.0')) self.pid_kd = float(self.cfg.get('pid_kd', '0.1')) def _parse_txt_config(self, path): """解析TXT格式的配置文件""" config = {} with open(path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue if '=' in line: key, value = line.split('=', 1) config[key.strip()] = value.strip() return config def _determine_device(self): """确定运行设备""" if self.model_device == 'auto': return 'cuda' if torch.cuda.is_available() and torch.cuda.device_count() > 0 else 'cpu' return self.model_device def _init_screen_info(self): """初始化屏幕信息""" user32 = ctypes.windll.user32 self.screen_width, self.screen_height = user32.GetSystemMetrics(0), user32.GetSystemMetrics(1) self.screen_center = (self.screen_width // 2, self.screen_height // 2) # 计算截图区域 left = (self.screen_width - self.screen_target_size) // 2 top = (self.screen_height - self.screen_target_size) // 2 self.region = ( max(0, int(left)), max(0, int(top)), min(self.screen_width, int(left + self.screen_target_size)), min(self.screen_height, int(top + self.screen_target_size)) ) def _init_control_params(self): """初始化控制参数""" self.pid_controller = PIDController( kp=self.pid_kp, ki=self.pid_ki, kd=self.pid_kd ) self.previous_target_info = None self.closest_target_absolute = None self.target_offset = None self.shift_pressed = False def _init_camera(self): """初始化相机""" try: with self.camera_lock: self.camera = dxcam.create( output_idx=0, output_color="BGR", region=self.region ) self.camera.start(target_fps=120, video_mode=True) except Exception as e: print(f"相机初始化失败: {str(e)}") try: # 降级模式 with self.camera_lock: self.camera = dxcam.create() self.camera.start(target_fps=60, video_mode=True) except Exception as fallback_e: print(f"降级模式初始化失败: {str(fallback_e)}") self.camera = None def _init_keyboard_listener(self): """初始化键盘监听""" self.keyboard_listener = keyboard.Listener( on_press=self.on_key_press, on_release=self.on_key_release ) self.keyboard_listener.daemon = True self.keyboard_listener.start() def on_key_press(self, key): """处理按键按下事件""" try: if key == keyboard.Key.shift: with self.shift_lock: self.shift_pressed = True self.pid_controller.reset() except Exception as e: print(f"键盘按下事件处理错误: {str(e)}") def on_key_release(self, key): """处理按键释放事件""" try: if key == keyboard.Key.shift: with self.shift_lock: self.shift_pressed = False except Exception as e: print(f"键盘释放事件处理错误: {str(e)}") def calculate_fov_movement(self, dx, dy): """基于FOV算法计算鼠标移动量""" # 计算屏幕对角线长度 screen_diagonal = (self.screen_width ** 2 + self.screen_height ** 2) ** 0.5 # 计算垂直FOV aspect_ratio = self.screen_width / self.screen_height fov_vertical = self.fov_horizontal / aspect_ratio # 计算每像素对应角度 angle_per_pixel_x = self.fov_horizontal / self.screen_width angle_per_pixel_y = fov_vertical / self.screen_height # 计算角度偏移 angle_offset_x = dx * angle_per_pixel_x angle_offset_y = dy * angle_per_pixel_y # 转换为鼠标移动量 move_x = (angle_offset_x / 360) * self.mouse_dpi move_y = (angle_offset_y / 360) * self.mouse_dpi return move_x, move_y def move_mouse_to_target(self): """移动鼠标使准心对准目标点""" if not self.target_offset: return try: # 获取目标点与屏幕中心的偏移量 with self.offset_lock: dx, dy = self.target_offset # 使用FOV算法计算鼠标移动量 move_x, move_y = self.calculate_fov_movement(dx, dy) # 使用PID控制器平滑移动 pid_output = self.pid_controller.update((move_x, move_y)) move_x_pid, move_y_pid = pid_output # 使用罗技API移动鼠标 if move_x_pid != 0 or move_y_pid != 0: logitech.lg.mouse_xy(int(move_x_pid), int(move_y_pid)) except Exception as e: print(f"移动鼠标时出错: {str(e)}") def run(self, frame_queue): """主检测循环""" while not self.stop_event.is_set(): try: # 截图 grab_start = time.perf_counter() screenshot = self._grab_screenshot() grab_time = (time.perf_counter() - grab_start) * 1000 # ms if screenshot is None: time.sleep(0.001) continue # 推理 inference_start = time.perf_counter() results = self._inference(screenshot) inference_time = (time.perf_counter() - inference_start) * 1000 # ms # 处理检测结果 target_info, closest_target_relative, closest_offset = self._process_detection_results(results) # 更新目标信息 self._update_target_info(target_info, closest_offset) # 移动鼠标 self._move_mouse_if_needed() # 可视化处理 annotated_frame = self._visualize_results(results, closest_target_relative) if frame_queue else None # 放入队列 if frame_queue: try: frame_queue.put( (annotated_frame, len(target_info), inference_time, grab_time, target_info), timeout=0.01 ) except queue.Full: pass except Exception as e: print(f"检测循环异常: {str(e)}") traceback.print_exc() self._reset_camera() time.sleep(0.5) def _grab_screenshot(self): """安全获取截图""" with self.camera_lock: if self.camera: return self.camera.grab() return None def _inference(self, screenshot): """执行模型推理""" return self.model.predict( screenshot, conf=self.detection_conf_thres, iou=self.detection_iou_thres, classes=self.detection_classes, device=self.device, verbose=False ) def _process_detection_results(self, results): """处理检测结果""" target_info = [] min_distance = float('inf') closest_target_relative = None closest_target_absolute = None closest_offset = None for box in results[0].boxes: # 获取边界框坐标 x1, y1, x2, y2 = map(int, box.xyxy[0]) # 计算绝对坐标 x1_abs = x1 + self.region[0] y1_abs = y1 + self.region[1] x2_abs = x2 + self.region[0] y2_abs = y2 + self.region[1] # 计算目标中心点 target_center = ((x1_abs + x2_abs) // 2, (y1_abs + y2_abs) // 2) # 计算偏移量 dx = target_center[0] - self.screen_center[0] dy = target_center[1] - self.screen_center[1] distance = (dx ** 2 + dy ** 2) ** 0.5 # 更新最近目标 if distance < min_distance: min_distance = distance closest_target_relative = ((x1 + x2) // 2, (y1 + y2) // 2) closest_target_absolute = target_center closest_offset = (dx, dy) # 保存目标信息 class_id = int(box.cls) class_name = self.model.names[class_id] target_info.append(f"{class_name}:{x1_abs},{y1_abs},{x2_abs},{y2_abs}") return target_info, closest_target_relative, closest_offset def _update_target_info(self, target_info, closest_offset): """更新目标信息""" # 检查目标信息是否有变化 if target_info != self.previous_target_info: self.previous_target_info = target_info.copy() print(f"{len(target_info)}|{'|'.join(target_info)}") # 更新目标偏移量 with self.offset_lock: self.target_offset = closest_offset def _visualize_results(self, results, closest_target): """可视化处理结果""" frame = results[0].plot( line_width=self.visualization_line_width, font_size=self.visualization_font_scale, conf=self.visualization_show_conf ) # 绘制最近目标 if closest_target: # 绘制目标中心点 cv2.circle( frame, (int(closest_target[0]), int(closest_target[1])), 3, (0, 0, 255), -1 ) # 计算屏幕中心在截图区域内的相对坐标 screen_center_x = self.screen_center[0] - self.region[0] screen_center_y = self.screen_center[1] - self.region[1] # 绘制中心到目标的连线 cv2.line( frame, (int(screen_center_x), int(screen_center_y)), (int(closest_target[0]), int(closest_target[1])), (0, 255, 0), 1 ) return frame def _move_mouse_if_needed(self): """如果需要则移动鼠标""" with self.shift_lock: if self.shift_pressed and self.target_offset: self.move_mouse_to_target() def _reset_camera(self): """重置相机""" print("正在重置相机...") try: self._init_camera() except Exception as e: print(f"相机重置失败: {str(e)}") traceback.print_exc() def stop(self): """安全停止检测器""" self.stop_event.set() self._safe_stop() if hasattr(self, 'keyboard_listener') and self.keyboard_listener.running: self.keyboard_listener.stop() def _safe_stop(self): """同步释放资源""" print("正在安全停止相机...") try: with self.camera_lock: if self.camera: self.camera.stop() print("相机已停止") except Exception as e: print(f"停止相机时发生错误: {str(e)}") print("屏幕检测器已停止") class App: def __init__(self, root, detector): self.root = root self.detector = detector self.root.title("DXcam Detection") self.root.geometry(f"{detector.region[2] - detector.region[0]}x{detector.region[3] - detector.region[1] + 50}") self.root.wm_attributes('-topmost', 1) # 界面组件 self.canvas = tk.Canvas(root, highlightthickness=0) self.canvas.pack(fill=tk.BOTH, expand=True) # 性能监控队列 self.frame_queue = queue.Queue(maxsize=3) # 控制面板 self.control_frame = tk.Frame(root) self.control_frame.pack(side=tk.BOTTOM, fill=tk.X) # 性能信息显示 self.info_label = tk.Label(self.control_frame, text="初始化中...", font=("Consolas", 10)) self.info_label.pack(side=tk.TOP, fill=tk.X, padx=5) # 按钮区域 self.toggle_btn = tk.Button(self.control_frame, text="切换可视化", command=self.toggle_visualization) self.toggle_btn.pack(side=tk.LEFT, padx=5) self.settings_btn = tk.Button(self.control_frame, text="设置", command=self.open_settings) self.settings_btn.pack(side=tk.LEFT, padx=5) # Shift键状态显示 self.shift_status = tk.Label(self.control_frame, text="Shift状态: 未按下", fg="red", font=("Consolas", 10)) self.shift_status.pack(side=tk.LEFT, padx=10) # 启动检测线程 self.detection_thread = threading.Thread(target=self.detector.run, args=(self.frame_queue,)) self.detection_thread.daemon = True self.detection_thread.start() # 界面更新 self.visualization_enabled = True self.update_interval = 1 # 1ms更新一次界面 self.update_image() # 窗口关闭处理 self.root.protocol("WM_DELETE_WINDOW", self.safe_exit) # 绑定键盘事件 self.root.bind('<KeyPress-Shift_L>', self.update_shift_status) self.root.bind('<KeyRelease-Shift_L>', self.update_shift_status) self.root.bind('<KeyPress-Shift_R>', self.update_shift_status) self.root.bind('<KeyRelease-Shift_R>', self.update_shift_status) def update_shift_status(self, event=None): """更新Shift键状态显示""" with self.detector.shift_lock: if self.detector.shift_pressed: self.shift_status.config(text="Shift状态: 按下", fg="green") else: self.shift_status.config(text="Shift状态: 未按下", fg="red") def toggle_visualization(self): """切换可视化状态""" self.visualization_enabled = not self.visualization_enabled state = "启用" if self.visualization_enabled else "禁用" self.info_label.config(text=f"可视化状态: {state}") self.canvas.delete("all") if not self.visualization_enabled: self.canvas.config(bg="black") def open_settings(self): """打开设置窗口""" SettingsWindow(self.root, self.detector.cfg) def display_target_info(self, target_info): """在画布上显示目标信息""" # 显示标题 title = "目标类别与坐标" self.canvas.create_text(10, 10, text=title, anchor=tk.NW, fill="#00FF00", font=("Consolas", 11, "bold")) # 显示目标信息 y_offset = 40 line_height = 20 if target_info: for i, data in enumerate(target_info): try: parts = data.split(":", 1) if len(parts) == 2: class_name, coords_str = parts coords = list(map(int, coords_str.split(','))) if len(coords) == 4: display_text = f"{class_name}: [{coords[0]}, {coords[1]}, {coords[2]}, {coords[3]}]" else: display_text = f"坐标格式错误: {data}" else: display_text = f"数据格式错误: {data}" except: display_text = f"解析错误: {data}" self.canvas.create_text(15, y_offset, text=display_text, anchor=tk.NW, fill="#00FFFF", font=("Consolas", 10)) y_offset += line_height else: self.canvas.create_text(15, y_offset, text="无检测目标", anchor=tk.NW, fill="#FF0000", font=("Consolas", 10)) def update_image(self): """更新界面显示""" try: # 获取最新数据 latest_data = None while not self.frame_queue.empty(): latest_data = self.frame_queue.get_nowait() if latest_data: # 解包数据 frame, targets_count, inference_time, grab_time, target_info = latest_data # 单位转换 inference_sec = inference_time / 1000 grab_sec = grab_time / 1000 # 更新显示 if self.visualization_enabled and frame is not None: # 显示图像 img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = Image.fromarray(img) self.tk_image = ImageTk.PhotoImage(image=img) self.canvas.delete("all") self.canvas.create_image(0, 0, image=self.tk_image, anchor=tk.NW) else: # 显示坐标文本 self.canvas.delete("all") self.display_target_info(target_info) # 更新性能信息 info_text = (f"目标: {targets_count} | " f"推理: {inference_sec:.3f}s | " f"截图: {grab_sec:.3f}s") self.info_label.config(text=info_text) except Exception as e: print(f"更新图像时出错: {str(e)}") finally: # 更新Shift键状态 self.update_shift_status() # 调度下一次更新 self.root.after(self.update_interval, self.update_image) def safe_exit(self): """安全退出程序""" self.detector.stop() self.root.after(100, self.root.destroy) class SettingsWindow(tk.Toplevel): def __init__(self, parent, config): super().__init__(parent) self.title("设置") self.geometry("400x450") self.config = config self.transient(parent) self.grab_set() self.create_widgets() def create_widgets(self): """创建设置窗口界面""" notebook = ttk.Notebook(self) notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 模型设置 model_frame = ttk.Frame(notebook) notebook.add(model_frame, text="模型设置") self.create_model_settings(model_frame) # 屏幕设置 screen_frame = ttk.Frame(notebook) notebook.add(screen_frame, text="屏幕设置") self.create_screen_settings(screen_frame) # 检测设置 detection_frame = ttk.Frame(notebook) notebook.add(detection_frame, text="检测设置") self.create_detection_settings(detection_frame) # 移动设置 move_frame = ttk.Frame(notebook) notebook.add(move_frame, text="移动设置") self.create_move_settings(move_frame) # 按钮区域 btn_frame = ttk.Frame(self) btn_frame.pack(fill=tk.X, padx=10, pady=10) save_btn = tk.Button(btn_frame, text="保存配置", command=self.save_config) save_btn.pack(side=tk.RIGHT, padx=5) cancel_btn = tk.Button(btn_frame, text="取消", command=self.destroy) cancel_btn.pack(side=tk.RIGHT, padx=5) def create_model_settings(self, parent): """创建模型设置页面""" # 获取基础路径 if getattr(sys, 'frozen', False): base_path = sys._MEIPASS else: base_path = os.path.dirname(os.path.abspath(__file__)) # 获取模型文件列表 models_dir = os.path.join(base_path, 'models') model_files = [] if os.path.exists(models_dir): model_files = glob.glob(os.path.join(models_dir, '*.pt')) # 处理模型显示名称 model_display_names = [os.path.basename(f) for f in model_files] if model_files else ["未找到模型文件"] self.model_name_to_path = {os.path.basename(f): f for f in model_files} # 当前配置的模型处理 current_model_path = self.config['model_path'] current_model_name = os.path.basename(current_model_path) # 确保当前模型在列表中 if current_model_name not in model_display_names: model_display_names.append(current_model_name) self.model_name_to_path[current_model_name] = current_model_path # 创建UI组件 ttk.Label(parent, text="选择模型:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) self.model_name = tk.StringVar(value=current_model_name) model_combo = ttk.Combobox(parent, textvariable=self.model_name, state="readonly", width=30) model_combo['values'] = model_display_names model_combo.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="运行设备:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) self.device_var = tk.StringVar(value=self.config['model_device']) device_combo = ttk.Combobox(parent, textvariable=self.device_var, state="readonly", width=30) device_combo['values'] = ('auto', 'cuda', 'cpu') device_combo.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W) def create_screen_settings(self, parent): """创建屏幕设置页面""" ttk.Label(parent, text="显示器编号:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) self.monitor_var = tk.StringVar(value=self.config.get('screen_monitor', '0')) ttk.Entry(parent, textvariable=self.monitor_var, width=10).grid(row=0, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="截屏尺寸:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) self.target_size_var = tk.StringVar(value=self.config['screen_target_size']) ttk.Entry(parent, textvariable=self.target_size_var, width=10).grid(row=1, column=1, padx=5, pady=5, sticky=tk.W) def create_detection_settings(self, parent): """创建检测设置页面""" ttk.Label(parent, text="置信度阈值:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) self.conf_thres_var = tk.DoubleVar(value=float(self.config['detection_conf_thres'])) conf_scale = ttk.Scale(parent, from_=0.1, to=1.0, variable=self.conf_thres_var, orient=tk.HORIZONTAL, length=200) conf_scale.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W) self.conf_thres_display = tk.StringVar() self.conf_thres_display.set(f"{self.conf_thres_var.get():.2f}") ttk.Label(parent, textvariable=self.conf_thres_display).grid(row=0, column=2, padx=5, pady=5) self.conf_thres_var.trace_add("write", lambda *args: self.conf_thres_display.set(f"{self.conf_thres_var.get():.2f}")) ttk.Label(parent, text="IOU阈值:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) self.iou_thres_var = tk.DoubleVar(value=float(self.config['detection_iou_thres'])) iou_scale = ttk.Scale(parent, from_=0.1, to=1.0, variable=self.iou_thres_var, orient=tk.HORIZONTAL, length=200) iou_scale.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W) self.iou_thres_display = tk.StringVar() self.iou_thres_display.set(f"{self.iou_thres_var.get():.2f}") ttk.Label(parent, textvariable=self.iou_thres_display).grid(row=1, column=2, padx=5, pady=5) self.iou_thres_var.trace_add("write", lambda *args: self.iou_thres_display.set(f"{self.iou_thres_var.get():.2f}")) ttk.Label(parent, text="检测类别:").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W) self.classes_var = tk.StringVar(value=self.config['detection_classes']) ttk.Entry(parent, textvariable=self.classes_var, width=20).grid(row=2, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="(逗号分隔)").grid(row=2, column=2, padx=5, pady=5, sticky=tk.W) def create_move_settings(self, parent): """创建移动设置页面""" ttk.Label(parent, text="横向FOV(度):").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) self.fov_horizontal_var = tk.StringVar(value=self.config.get('move_fov_horizontal', '90')) fov_entry = ttk.Entry(parent, textvariable=self.fov_horizontal_var, width=10) fov_entry.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="鼠标DPI:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) self.mouse_dpi_var = tk.StringVar(value=self.config.get('move_mouse_dpi', '400')) dpi_entry = ttk.Entry(parent, textvariable=self.mouse_dpi_var, width=10) dpi_entry.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W) # PID参数设置 ttk.Label(parent, text="PID参数设置", font=("Arial", 10, "bold")).grid(row=2, column=0, columnspan=3, pady=10, sticky=tk.W) ttk.Label(parent, text="比例系数(P):").grid(row=3, column=0, padx=5, pady=5, sticky=tk.W) self.pid_kp_var = tk.StringVar(value=self.config.get('pid_kp', '0.5')) kp_entry = ttk.Entry(parent, textvariable=self.pid_kp_var, width=10) kp_entry.grid(row=3, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="积分系数(I):").grid(row=4, column=0, padx=5, pady=5, sticky=tk.W) self.pid_ki_var = tk.StringVar(value=self.config.get('pid_ki', '0.0')) ki_entry = ttk.Entry(parent, textvariable=self.pid_ki_var, width=10) ki_entry.grid(row=4, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="微分系数(D):").grid(row=5, column=0, padx=5, pady=5, sticky=tk.W) self.pid_kd_var = tk.StringVar(value=self.config.get('pid_kd', '0.1')) kd_entry = ttk.Entry(parent, textvariable=self.pid_kd_var, width=10) kd_entry.grid(row=5, column=1, padx=5, pady=5, sticky=tk.W) def save_config(self): """保存配置到文件""" try: model_name = self.model_name.get() model_path = self.model_name_to_path.get(model_name, model_name) self.config['model_path'] = model_path self.config['model_device'] = self.device_var.get() self.config['screen_monitor'] = self.monitor_var.get() self.config['screen_target_size'] = self.target_size_var.get() self.config['detection_conf_thres'] = str(self.conf_thres_var.get()) self.config['detection_iou_thres'] = str(self.iou_thres_var.get()) self.config['detection_classes'] = self.classes_var.get() # 保存移动设置 self.config['move_fov_horizontal'] = self.fov_horizontal_var.get() self.config['move_mouse_dpi'] = self.mouse_dpi_var.get() # 保存PID参数 self.config['pid_kp'] = self.pid_kp_var.get() self.config['pid_ki'] = self.pid_ki_var.get() self.config['pid_kd'] = self.pid_kd_var.get() # 保存为TXT格式 with open('detection_config.txt', 'w', encoding='utf-8') as f: for key, value in self.config.items(): f.write(f"{key} = {value}\n") messagebox.showinfo("成功", "配置已保存!重启后生效") self.destroy() except Exception as e: messagebox.showerror("错误", f"保存配置失败: {str(e)}") if __name__ == "__main__": detector = ScreenDetector('detection_config.txt') print(f"\nDXcam检测器初始化完成 | 设备: {detector.device.upper()}") root = tk.Tk() app = App(root, detector) root.mainloop() 我需要添加对目标点进行平滑处理的函数,让目标点不抖动。注意是封装好。然后在我推理,检测结果之后再平滑处理,再更新目标信息。注意不是目标框平滑,是目标点平滑。请你严格根据我的代码风格来。把需要修改的函数和需要添加和函数做详细说明
最新发布
07-04
<think>我们将在ScreenDetector类中添加一个用于目标点平滑处理的函数。我们将使用一个简单的指数移动平均(EMA)滤波器来平滑目标点的位置。平滑处理将在检测到目标点之后,更新目标信息之前进行。具体步骤:1.在ScreenDetector类中初始化平滑滤波器所需的参数(如平滑系数alpha)。2.添加一个函数用于对目标点进行平滑处理(例如`smooth_target_point`)。3.在`_process_detection_results`函数中,对检测到的最近目标点进行平滑处理。4.使用平滑后的目标点来计算偏移量,并更新目标信息。注意:由于我们是对目标点(即中心点)进行平滑,而不是目标框,所以我们将对每个目标的中心点坐标进行平滑。修改点:-在`__init__`方法中添加平滑滤波器的初始化(如平滑系数alpha,以及用于存储上一次平滑结果的变量)。-修改`_process_detection_results`方法,在找到最近目标点后,对其进行平滑处理,然后使用平滑后的点计算偏移量。考虑到可能有多个目标,但我们只对最近的一个目标进行平滑(因为只有最近的目标会被用于移动鼠标)。另外,为了避免在刚开始时没有目标导致的错误,我们需要在初始化时设置一个初始状态(例如None),并在第一次检测到目标时初始化平滑值。我们将添加以下成员变量:self.smoothed_target_center:用于存储上一次平滑后的目标中心点(绝对坐标)self.alpha:平滑系数(0~1之间的值,值越大表示对当前值越信任,平滑效果越弱)在配置文件中,我们可以添加一个配置项`smooth_alpha`,用于设置平滑系数。修改配置文件解析部分,读取平滑系数(如果没有则使用默认值0.5)。在`_process_detection_results`方法中,当我们找到最近目标点(closest_target_absolute)后,进行平滑处理:如果这是第一次检测到目标(即self.smoothed_target_center为None),则直接使用当前目标点作为平滑值。否则,使用EMA公式:smoothed= alpha* current+ (1 -alpha)* previous然后,使用平滑后的目标点来计算偏移量(dx, dy)并返回。注意:当没有检测到目标时,我们需要重置平滑状态(将self.smoothed_target_center设为None),以免影响下一次的平滑。同时,在更新目标信息后,我们需要保存平滑后的目标点(用于下一次平滑)。但是注意,在每次检测循环中,我们只对最近的一个目标进行平滑。如果这一帧没有目标,那么我们就不会进行平滑,并且将平滑状态重置。另外,当目标丢失时,我们重置平滑状态。具体修改如下:1.在ScreenDetector的`__init__`方法中:-从配置文件中读取平滑系数`smooth_alpha`(默认0.5)-初始化self.alpha =配置值-初始化self.smoothed_target_center =None#用于存储平滑后的目标中心(绝对坐标)2.在`_process_detection_results`方法中,处理完所有目标后(即找到最近目标后):-如果找到了最近目标(closest_target_absolute不为None):如果self.smoothed_target_center为None(第一次检测到目标),则将其初始化为当前目标点。否则,使用EMA进行平滑:smoothed_x =self.alpha *closest_target_absolute[0] +(1- self.alpha) *self.smoothed_target_center[0]smoothed_y=self.alpha *closest_target_absolute[1] +(1- self.alpha) *self.smoothed_target_center[1]更新self.smoothed_target_center =(smoothed_x,smoothed_y)然后使用平滑后的点(self.smoothed_target_center)来计算偏移量(相对于屏幕中心)-如果没有找到目标,则重置self.smoothed_target_center=None,并设置closest_offset=None3.注意:在计算偏移量时,我们使用平滑后的目标点(self.smoothed_target_center)和屏幕中心(self.screen_center)的差值。4.同时,我们返回的closest_target_absolute应该是平滑后的点(用于可视化时显示平滑后的点),但注意在可视化时,我们绘制的是相对坐标(截图区域内的坐标),所以需要将平滑后的绝对坐标转换为相对坐标。5.在可视化部分,我们原本使用的是closest_target_relative(即未平滑前的相对坐标)来绘制中心点和连线。现在,我们使用平滑后的目标点(绝对坐标)减去截图区域的左上角坐标,得到相对坐标,用于绘制。因此,我们还需要在`_process_detection_results`中计算平滑后的目标点的相对坐标,并返回(用于可视化)。修改后的`_process_detection_results`函数将返回:target_info:目标信息列表(不变)closest_target_relative_smoothed:平滑后的目标点在截图区域内的相对坐标(用于可视化)closest_offset:平滑后的目标点相对于屏幕中心的偏移量(用于移动鼠标)注意:如果没有目标,则返回的closest_target_relative_smoothed和closest_offset都为None。具体步骤:1.在找到最近目标点(closest_target_absolute)后,进行平滑处理,得到平滑后的绝对坐标(smoothed_absolute)。2.计算平滑后的相对坐标(用于可视化):smoothed_relative_x =smoothed_absolute[0]- self.region[0]smoothed_relative_y =smoothed_absolute[1]- self.region[1]3.计算偏移量(相对于屏幕中心):dx= smoothed_absolute[0]- self.screen_center[0]dy= smoothed_absolute[1] -self.screen_center[1]4.如果没有目标,则重置平滑状态,并设置相关变量为None。这样,我们就完成了目标点的平滑处理。另外,在配置文件中添加平滑系数的配置项,并在解析配置时读取。修改`_parse_config`方法,添加:self.smooth_alpha =float(self.cfg.get('smooth_alpha', '0.5'))同时,在`_init_control_params`中初始化:self.smoothed_target_center =None下面我们修改代码:注意:由于平滑处理可能会引入延迟,但可以减少抖动。我们可以通过调整alpha值来控制平滑程度。另外,在目标丢失时重置平滑状态,这样当目标重新出现时,平滑会重新开始(而不是使用很久之前的数据)。修改代码:</think>我将为你的代码添加目标点平滑处理功能,以减少目标点的抖动。以下是详细的修改方案: ### 需要添加/修改的部分: 1. **添加平滑处理类**:创建一个新的 `SmoothingFilter` 类来处理目标点的平滑 2. **修改 `ScreenDetector` 类**: - 初始化平滑过滤器 - 在检测结果处理后添加平滑步骤 3. **更新配置文件**:添加平滑相关的配置参数 ### 具体代码实现: #### 1. 添加平滑处理类(在 `PIDController` 类之后添加) ```python class SmoothingFilter: """目标点平滑过滤器""" def __init__(self, alpha=0.5, beta=0.5, min_distance=5): """ 初始化平滑过滤器 :param alpha: 位置平滑系数 (0-1) :param beta: 速度平滑系数 (0-1) :param min_distance: 最小移动距离阈值 """ self.alpha = alpha self.beta = beta self.min_distance = min_distance self.prev_position = None self.prev_velocity = (0, 0) self.smoothed_position = None def update(self, position): """更新目标位置并返回平滑后的位置""" if position is None: # 重置状态 self.prev_position = None self.prev_velocity = (0, 0) return None if self.prev_position is None: # 第一次检测,直接使用当前值 self.prev_position = position self.smoothed_position = position return position # 计算当前位置与上次位置的差值 dx = position[0] - self.prev_position[0] dy = position[1] - self.prev_position[1] # 计算速度 current_velocity = (dx, dy) # 平滑速度 smoothed_velocity = ( self.beta * current_velocity[0] + (1 - self.beta) * self.prev_velocity[0], self.beta * current_velocity[1] + (1 - self.beta) * self.prev_velocity[1] ) # 预测位置 = 上次位置 + 平滑速度 predicted_position = ( self.prev_position[0] + smoothed_velocity[0], self.prev_position[1] + smoothed_velocity[1] ) # 平滑位置 = alpha * 实际位置 + (1 - alpha) * 预测位置 smoothed_position = ( self.alpha * position[0] + (1 - self.alpha) * predicted_position[0], self.alpha * position[1] + (1 - self.alpha) * predicted_position[1] ) # 检查移动距离是否超过阈值 distance = ((smoothed_position[0] - self.prev_position[0])**2 + (smoothed_position[1] - self.prev_position[1])**2)**0.5 if distance < self.min_distance: # 微小移动,保持上次位置 smoothed_position = self.prev_position # 更新状态 self.prev_position = position self.prev_velocity = smoothed_velocity self.smoothed_position = smoothed_position return smoothed_position def reset(self): """重置过滤器状态""" self.prev_position = None self.prev_velocity = (0, 0) self.smoothed_position = None ``` #### 2. 修改 `ScreenDetector` 类 在 `__init__` 方法中添加平滑过滤器的初始化: ```python class ScreenDetector: def __init__(self, config_path): # ... 原有代码 ... # 平滑过滤器初始化 self._init_smoothing_filter() # ... 原有代码 ... def _init_smoothing_filter(self): """初始化平滑过滤器""" # 从配置中获取参数 smooth_alpha = float(self.cfg.get('smooth_alpha', '0.7')) smooth_beta = float(self.cfg.get('smooth_beta', '0.5')) smooth_min_distance = int(self.cfg.get('smooth_min_distance', '5')) # 创建平滑过滤器 self.smoothing_filter = SmoothingFilter( alpha=smooth_alpha, beta=smooth_beta, min_distance=smooth_min_distance ) ``` 在 `_process_detection_results` 方法中添加平滑处理步骤: ```python def _process_detection_results(self, results): """处理检测结果""" # ... 原有代码 ... # 应用平滑处理 if closest_target_absolute: smoothed_target = self.smoothing_filter.update(closest_target_absolute) # 计算平滑后的偏移量 dx_smoothed = smoothed_target[0] - self.screen_center[0] dy_smoothed = smoothed_target[1] - self.screen_center[1] # 更新返回的值 closest_target_absolute = smoothed_target closest_offset = (dx_smoothed, dy_smoothed) # 计算平滑后的相对坐标(用于可视化) closest_target_relative = ( smoothed_target[0] - self.region[0], smoothed_target[1] - self.region[1] ) else: # 没有目标时重置过滤器 self.smoothing_filter.reset() return target_info, closest_target_relative, closest_offset ``` 在目标丢失时重置过滤器(在 `run` 方法中): ```python def run(self, frame_queue): """主检测循环""" while not self.stop_event.is_set(): try: # ... 原有代码 ... # 处理检测结果 target_info, closest_target_relative, closest_offset = self._process_detection_results(results) # 如果没有检测到目标,重置平滑过滤器 if not target_info: self.smoothing_filter.reset() # ... 原有代码 ... ``` #### 3. 更新配置文件解析 在 `_parse_config` 方法中添加平滑参数的解析: ```python def _parse_config(self, config_path): """解析并存储配置参数""" # ... 原有代码 ... # 添加平滑参数 self.smooth_alpha = float(self.cfg.get('smooth_alpha', '0.7')) self.smooth_beta = float(self.cfg.get('smooth_beta', '0.5')) self.smooth_min_distance = int(self.cfg.get('smooth_min_distance', '5')) ``` ### 配置文件中添加的参数 在 `detection_config.txt` 中添加以下参数: ``` # 平滑参数 smooth_alpha = 0.7 # 位置平滑系数 (0-1) smooth_beta = 0.5 # 速度平滑系数 (0-1) smooth_min_distance = 5 # 最小移动距离阈值(像素) ``` ### 修改后的 `_process_detection_results` 完整代码 ```python def _process_detection_results(self, results): """处理检测结果""" target_info = [] min_distance = float('inf') closest_target_relative = None closest_target_absolute = None closest_offset = None for box in results[0].boxes: # ... 原有代码(计算目标位置)... # 应用平滑处理 if closest_target_absolute: smoothed_target = self.smoothing_filter.update(closest_target_absolute) # 计算平滑后的偏移量 dx_smoothed = smoothed_target[0] - self.screen_center[0] dy_smoothed = smoothed_target[1] - self.screen_center[1] # 更新返回的值 closest_target_absolute = smoothed_target closest_offset = (dx_smoothed, dy_smoothed) # 计算平滑后的相对坐标(用于可视化) closest_target_relative = ( smoothed_target[0] - self.region[0], smoothed_target[1] - self.region[1] ) else: # 没有目标时重置过滤器 self.smoothing_filter.reset() return target_info, closest_target_relative, closest_offset ``` ### 平滑处理工作原理 1. **双重平滑机制**: - **位置平滑**:使用指数平滑减少位置抖动 - **速度平滑**:预测目标移动方向,减少方向突变 2. **微小移动过滤**: - 当目标移动距离小于阈值(`min_distance`)时,保持上次位置 - 防止由于检测抖动导致的微小移动 3. **状态重置**: - 当目标丢失时重置过滤器状态 - 避免上次目标的位置影响新目标的检测 ### 注意事项 1. **参数调整**: - `alpha` 值越大,对当前位置的信任度越高(平滑效果弱) - `alpha` 值越小,平滑效果越强(响应延迟增加) - 建议从 `0.7` 开始调整,根据实际效果优化 2. **性能影响**: - 平滑处理计算量很小,不会显著影响性能 - 如果遇到性能问题,可适当增大 `min_distance` 值 3. **可视化效果**: - 平滑后的目标点会显示为红色圆点(原代码已实现) - 连线也会使用平滑后的坐标 这些修改将显著减少目标点的抖动,特别是在目标移动缓慢或检测结果有轻微波动的情况下,同时保持了系统的响应速度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值