Python宏脚本W/S按键模拟速度不符设置问题求助
问题排查与解决方案
你的W/S按键模拟速度远低于设定值,核心原因是图像检测操作占用了绝大多数运行时间,按键逻辑被频繁阻塞,无法按1ms间隔执行。以下是具体分析和优化方案:
关键问题点
- 图像检测耗时过高:每次循环都会执行两次全屏截图+CV2模板匹配(检测X、Y图像),这两个操作本身就需要几百毫秒甚至更久,直接拖慢了整个循环的执行速度,按键逻辑自然无法按设定间隔运行。
- 串行执行优先级混乱:按键逻辑和图像检测在同一个线程里串行执行,高耗时的检测操作直接导致按键触发被延迟。
优化方案
1. 分离图像检测与按键逻辑到独立线程
把图像检测放到单独线程中,让按键逻辑不受检测耗时影响,保持高频执行。
2. 降低图像检测频率
不需要每毫秒都检测图像,设置合理的检测间隔(比如100ms一次),既保证不会错过触发条件,又减少性能消耗。
3. 缩小截图区域(可选)
如果知道目标图像出现的大致位置,只截取对应区域的屏幕,大幅减少截图和模板匹配的处理时间。
修改后的代码
import pyautogui import cv2 import numpy as np import tkinter as tk from tkinter import filedialog import threading import os import time import pygame from pynput.keyboard import Key, Controller, Listener import atexit import random # Inicializa o pygame para tocar os sons pygame.mixer.init() # Sons som_macro = pygame.mixer.Sound('start.wav') # Som para iniciar/parar o macro som_imagem_y = pygame.mixer.Sound('stop.wav') # Som para quando a imagem Y for detectada # Variáveis globais macro_rodando = False diretorio_imagem_y = None diretorio_imagem_x = os.path.join(os.getcwd(), "imagemX.png") # Caminho da imagem X no diretório do script teclas_alternancia = ['w', 's'] # Teclas para alternar botao_imagem_x = '4' # Botão para pressionar quando a imagem X for detectada tempo_alternancia = 0.001 # Tempo de alternância em segundos (inicialmente 1 ms) intervalo_deteccao = 0.1 # Intervalo entre detecções de imagem (100ms) # Inicializa o controlador de teclado controlador = Controller() listener = None # Função para pressionar e soltar uma tecla def pressionar_tecla(tecla): controlador.press(tecla) controlador.release(tecla) # Função para detectar a imagem na tela def detectar_imagem(diretorio, area=None): try: if not os.path.exists(diretorio): raise ValueError(f"Arquivo de imagem não encontrado: {diretorio}") diretorio = os.path.normpath(diretorio) imagem_modelo = cv2.imdecode(np.fromfile(diretorio, dtype=np.uint8), cv2.IMREAD_COLOR) if imagem_modelo is None: raise ValueError(f"Não foi possível carregar a imagem: {diretorio}") # 截取指定区域或全屏 tela = pyautogui.screenshot(region=area) if area else pyautogui.screenshot() tela = cv2.cvtColor(np.array(tela), cv2.COLOR_RGB2BGR) resultado = cv2.matchTemplate(tela, imagem_modelo, cv2.TM_CCOEFF_NORMED) _, max_valor, _, _ = cv2.minMaxLoc(resultado) return max_valor > 0.8 except Exception as e: print(f"Erro ao detectar a imagem: {e}") return False # 线程:处理图像检测 def thread_deteccao_imagens(): global macro_rodando ultima_deteccao = time.perf_counter() while macro_rodando: tempo_atual = time.perf_counter() if tempo_atual - ultima_deteccao >= intervalo_deteccao: # 检测Y图像(触发停止) if diretorio_imagem_y and detectar_imagem(diretorio_imagem_y): print("Imagem Y detectada. Parando a macro.") pygame.mixer.Sound.play(som_imagem_y) macro_rodando = False break # 检测X图像(触发按键) if detectar_imagem(diretorio_imagem_x): print(f"Imagem X detectada. Clicando {botao_imagem_x}.") pressionar_tecla(botao_imagem_x) ultima_deteccao = tempo_atual time.sleep(0.005) # 减少线程忙等待 # Função da macro(仅处理按键逻辑) def executar_macro(): global macro_rodando, tempo_alternancia ultimo_tecla = None ultimo_tempo = time.perf_counter() # 启动图像检测线程 threading.Thread(target=thread_deteccao_imagens, daemon=True).start() while macro_rodando: tempo_atual = time.perf_counter() if tempo_atual - ultimo_tempo >= tempo_alternancia: # Escolhe uma tecla aleatória, diferente da última tecla pressionada teclas_disponiveis = [tecla for tecla in teclas_alternancia if tecla != ultimo_tecla] if teclas_disponiveis: tecla_atual = random.choice(teclas_disponiveis) pressionar_tecla(tecla_atual) ultimo_tecla = tecla_atual ultimo_tempo = tempo_atual # 添加微小睡眠避免CPU占用过高 time.sleep(0.0001) # Função para iniciar/parar a macro def toggle_macro(): global macro_rodando pygame.mixer.Sound.play(som_macro) if macro_rodando: print("Parando macro.") macro_rodando = False else: print("Iniciando macro.") macro_rodando = True threading.Thread(target=executar_macro, daemon=True).start() # Função para selecionar o diretório da imagem Y def selecionar_imagem_y(): global diretorio_imagem_y diretorio_imagem_y = filedialog.askopenfilename(title="Selecione a imagem Y") if diretorio_imagem_y: diretorio_imagem_y = os.path.abspath(diretorio_imagem_y) print(f"Imagem Y selecionada: {diretorio_imagem_y}") # Função para selecionar o diretório da imagem X def selecionar_imagem_x(): global diretorio_imagem_x diretorio_imagem_x = filedialog.askopenfilename(title="Selecione a imagem X") if diretorio_imagem_x: diretorio_imagem_x = os.path.abspath(diretorio_imagem_x) print(f"Imagem X selecionada: {diretorio_imagem_x}") # Função para configurar as teclas e o botão def configurar(): def salvar_configuracoes(): global teclas_alternancia, botao_imagem_x, tempo_alternancia, intervalo_deteccao teclas_alternancia = [tecla.strip() for tecla in entrada_teclas_alternancia.get().split(',')] botao_imagem_x = entrada_botao.get().strip() try: tempo_alternancia = float(entrada_tempo.get().strip()) except ValueError: print("Valor inválido para tempo de alternância. Usando o valor padrão.") tempo_alternancia = 0.001 try: intervalo_deteccao = float(entrada_intervalo_deteccao.get().strip()) except ValueError: print("Valor inválido para intervalo de detecção. Usando o valor padrão.") intervalo_deteccao = 0.1 print(f"Configurações atualizadas: Teclas Alternância: {teclas_alternancia}, Botão Imagem X: {botao_imagem_x}, Tempo de Alternância: {tempo_alternancia} segundos, Intervalo Detecção: {intervalo_deteccao} segundos") configuracoes_window.destroy() configuracoes_window = tk.Toplevel(root) configuracoes_window.title("Configurações") configuracoes_window.configure(bg='#6A0D91') configuracoes_window.attributes('-topmost', True) configuracoes_window.resizable(False, False) # Remove o tamanho fixo para permitir ajuste automático configuracoes_window.grid_columnconfigure(0, weight=1) configuracoes_window.grid_columnconfigure(1, weight=3) configuracoes_window.grid_rowconfigure(0, weight=1) configuracoes_window.grid_rowconfigure(1, weight=1) configuracoes_window.grid_rowconfigure(2, weight=1) configuracoes_window.grid_rowconfigure(3, weight=1) configuracoes_window.grid_rowconfigure(4, weight=1) label_teclas_alternancia = tk.Label(configuracoes_window, text="Teclas Alternância (separadas por vírgula e espaço):", bg='#6A0D91', fg='white') label_teclas_alternancia.grid(row=0, column=0, padx=10, pady=5, sticky="w") entrada_teclas_alternancia = tk.Entry(configuracoes_window) entrada_teclas_alternancia.insert(0, ', '.join(teclas_alternancia)) entrada_teclas_alternancia.grid(row=0, column=1, padx=10, pady=5, sticky="ew") label_botao = tk.Label(configuracoes_window, text="Botão Imagem X:", bg='#6A0D91', fg='white') label_botao.grid(row=1, column=0, padx=10, pady=5, sticky="w") entrada_botao = tk.Entry(configuracoes_window) entrada_botao.insert(0, botao_imagem_x) entrada_botao.grid(row=1, column=1, padx=10, pady=5, sticky="ew") label_tempo = tk.Label(configuracoes_window, text="Tempo de Alternância (em segundos):", bg='#6A0D91', fg='white') label_tempo.grid(row=2, column=0, padx=10, pady=5, sticky="w") entrada_tempo = tk.Entry(configuracoes_window) entrada_tempo.insert(0, str(tempo_alternancia)) entrada_tempo.grid(row=2, column=1, padx=10, pady=5, sticky="ew") label_intervalo_deteccao = tk.Label(configuracoes_window, text="Intervalo de Detecção (em segundos):", bg='#6A0D91', fg='white') label_intervalo_deteccao.grid(row=3, column=0, padx=10, pady=5, sticky="w") entrada_intervalo_deteccao = tk.Entry(configuracoes_window) entrada_intervalo_deteccao.insert(0, str(intervalo_deteccao)) entrada_intervalo_deteccao.grid(row=3, column=1, padx=10, pady=5, sticky="ew") botao_salvar = tk.Button(configuracoes_window, text="Salvar Configurações", command=salvar_configuracoes, bg='#4B0082', fg='white') botao_salvar.grid(row=4, column=0, columnspan=2, pady=10) # Função para escutar teclas def on_press(key): if key == Key.f8: toggle_macro() # Função para encerrar o programa limpo def cleanup(): global listener, macro_rodando macro_rodando = False if listener: listener.stop() pygame.mixer.quit() cv2.destroyAllWindows() print("Programa encerrado corretamente.") # GUI principal def criar_gui(): global root root = tk.Tk() root.title("Macro de Imagens") # Configura a cor de fundo e outras propriedades root.configure(bg='#6A0D91') # Cor de fundo roxa # Força a janela a ficar sempre no topo root.attributes('-topmost', True) # Impede o redimensionamento da janela root.resizable(False, False) # Configura o estilo dos widgets label = tk.Label(root, text="Selecione a Imagem Y para parar a macro:", bg='#6A0D91', fg='white', font=('Arial', 12)) label.pack(pady=10) botao_selecionar_x = tk.Button(root, text="Selecionar Imagem X", command=selecionar_imagem_x, bg='#4B0082', fg='white', font=('Arial', 12)) botao_selecionar_x.pack(pady=10) botao_selecionar_y = tk.Button(root, text="Selecionar Imagem Y", command=selecionar_imagem_y, bg='#4B0082', fg='white', font=('Arial', 12)) botao_selecionar_y.pack(pady=10) botao_configurar = tk.Button(root, text="Configurar Teclas e Botão", command=configurar, bg='#4B0082', fg='white', font=('Arial', 12)) botao_configurar.pack(pady=10) root.mainloop() # Inicia o listener de teclas listener = Listener(on_press=on_press) listener.start() # Inicia a interface gráfica criar_gui() # Registra a função de limpeza para ser chamada ao sair atexit.register(cleanup)
额外优化建议
- 缩小截图区域:如果知道目标图像的位置,在
detectar_imagem函数中传入area=(x, y, largura, altura)参数,比如detectar_imagem(diretorio_imagem_x, area=(100, 200, 300, 300)),能大幅降低处理时间。 - 降低模板匹配精度要求:如果图像检测不需要极高精度,可以把
max_valor > 0.8调低到0.7或0.6,减少计算量。 - 使用更快的截图工具:替换
pyautogui.screenshot()为mss库,截图速度会显著提升,安装方式:pip install mss。
内容的提问来源于stack exchange,提问作者Gabriel Souza
相关产品推荐
相关产品推荐

