INIT
6
data/training/README.dataset.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
# DNF > 2023-06-06 3:57pm
|
||||
https://universe.roboflow.com/atomade/dnf-fzffw
|
||||
|
||||
Provided by a Roboflow user
|
||||
License: CC BY 4.0
|
||||
|
||||
36
data/training/README.roboflow.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
DNF - v1 2023-06-06 3:57pm
|
||||
==============================
|
||||
|
||||
This dataset was exported via roboflow.com on June 6, 2023 at 8:01 AM GMT
|
||||
|
||||
Roboflow is an end-to-end computer vision platform that helps you
|
||||
* collaborate with your team on computer vision projects
|
||||
* collect & organize images
|
||||
* understand and search unstructured image data
|
||||
* annotate, and create datasets
|
||||
* export, train, and deploy computer vision models
|
||||
* use active learning to improve your dataset over time
|
||||
|
||||
For state of the art Computer Vision training notebooks you can use with this dataset,
|
||||
visit https://github.com/roboflow/notebooks
|
||||
|
||||
To find over 100k other datasets and pre-trained models, visit https://universe.roboflow.com
|
||||
|
||||
The dataset includes 649 images.
|
||||
Player-gold-monster-boss are annotated in YOLOv8 format.
|
||||
|
||||
The following pre-processing was applied to each image:
|
||||
* Auto-orientation of pixel data (with EXIF-orientation stripping)
|
||||
* Resize to 640x640 (Stretch)
|
||||
|
||||
The following augmentation was applied to create 3 versions of each source image:
|
||||
* Randomly crop between 0 and 40 percent of the image
|
||||
* Random brigthness adjustment of between -30 and +30 percent
|
||||
* Random exposure adjustment of between -20 and +20 percent
|
||||
* Random Gaussian blur of between 0 and 1 pixels
|
||||
|
||||
The following transformations were applied to the bounding boxes of each image:
|
||||
* Salt and pepper noise was applied to 5 percent of pixels
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
path: /workspace/dnf-auto-cloud/data/training
|
||||
train: /workspace/dnf-auto-cloud/data/training/images/train
|
||||
val: /workspace/dnf-auto-cloud/data/training/images/val
|
||||
test: /workspace/dnf-auto-cloud/data/training/images/test
|
||||
nc: 10
|
||||
names:
|
||||
- monster
|
||||
- boss
|
||||
- door
|
||||
- item
|
||||
- npc
|
||||
- player
|
||||
- hp_bar
|
||||
- mp_bar
|
||||
- skill_ready
|
||||
- cooldown
|
||||
train: images/train
|
||||
val: images/val
|
||||
test: images/test
|
||||
nc: 15
|
||||
names: ['door_boss', 'door_boss_activated', 'door_normal', 'door_normal_activated', 'item_diamondcoin_advanced', 'item_gold', 'monster_hebron_archer', 'monster_hebron_boss', 'monster_hebron_bulbhead', 'monster_hebron_robot', 'monster_hebron_stone', 'monster_hebron_tortoise', 'player_spectre', 'window_continue', 'window_result']
|
||||
roboflow:
|
||||
workspace: atomade
|
||||
project: dnf-fzffw
|
||||
version: 1
|
||||
license: CC BY 4.0
|
||||
url: https://universe.roboflow.com/atomade/dnf-fzffw/dataset/1
|
||||
@@ -1,9 +0,0 @@
|
||||
DNF自动化客户端
|
||||
|
||||
安装说明:
|
||||
1. 安装Python 3.8或更高版本
|
||||
2. 安装依赖包:
|
||||
pip install pillow numpy websockets keyboard mouse pywin32 mss
|
||||
|
||||
3. 编辑config.ini设置服务器地址
|
||||
4. 运行start.bat启动客户端
|
||||
@@ -1,837 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
DNF自动化客户端,负责截取游戏画面并执行服务器返回的操作
|
||||
优化版 - 增加断线重连、性能优化和增强的游戏状态管理
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import time
|
||||
import random
|
||||
import asyncio
|
||||
import websockets
|
||||
import configparser
|
||||
import ssl
|
||||
import ctypes
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
import threading
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
# 图像处理
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
# Windows API
|
||||
import win32gui
|
||||
import win32con
|
||||
import win32api
|
||||
import win32process
|
||||
|
||||
# 输入模拟
|
||||
import keyboard
|
||||
import mouse
|
||||
|
||||
# 尝试导入mss库,如果不存在则继续使用PIL
|
||||
try:
|
||||
import mss
|
||||
MSS_AVAILABLE = True
|
||||
except ImportError:
|
||||
MSS_AVAILABLE = False
|
||||
print("警告: mss库未安装,将使用PIL进行截图(性能较低)")
|
||||
print("请使用 pip install mss 安装以获得更好的性能")
|
||||
|
||||
# 配置文件路径 - 使用绝对路径
|
||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.ini")
|
||||
|
||||
# 日志设置
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
handlers=[
|
||||
logging.FileHandler(os.path.join(os.path.dirname(os.path.abspath(__file__)), "client.log"), encoding="utf-8"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger("DNFAutoClient")
|
||||
|
||||
class DNFAutoClient:
|
||||
"""DNF自动化客户端类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化客户端"""
|
||||
self.config = self.load_config()
|
||||
self.server_url = self.config.get("Server", "url")
|
||||
self.client_id = None
|
||||
self.running = False
|
||||
self.ws = None
|
||||
self.capture_interval = float(self.config.get("Capture", "interval"))
|
||||
self.max_retries = int(self.config.get("Connection", "max_retries", fallback="5"))
|
||||
self.retry_delay = int(self.config.get("Connection", "retry_delay", fallback="5"))
|
||||
|
||||
# 增强的游戏状态
|
||||
self.game_state = {
|
||||
"in_battle": False,
|
||||
"current_map": "",
|
||||
"hp_percent": 100,
|
||||
"mp_percent": 100,
|
||||
"active_buffs": [],
|
||||
"cooldowns": {},
|
||||
"inventory_full": False,
|
||||
"current_quest": None,
|
||||
"last_operation_time": time.time(),
|
||||
"session_start_time": time.time()
|
||||
}
|
||||
|
||||
# 连接状态
|
||||
self.last_heartbeat_time = 0
|
||||
self.connection_attempts = 0
|
||||
self.reconnecting = False
|
||||
|
||||
def load_config(self):
|
||||
"""加载配置文件"""
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
self.create_default_config()
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(CONFIG_FILE, encoding="utf-8")
|
||||
return config
|
||||
|
||||
def create_default_config(self):
|
||||
"""创建默认配置文件"""
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
config["Server"] = {
|
||||
"url": "wss://your-server-url:8080/ws",
|
||||
"verify_ssl": "false"
|
||||
}
|
||||
|
||||
config["Capture"] = {
|
||||
"interval": "0.5",
|
||||
"quality": "70"
|
||||
}
|
||||
|
||||
config["Game"] = {
|
||||
"window_title": "地下城与勇士",
|
||||
"key_mapping": "default"
|
||||
}
|
||||
|
||||
config["Connection"] = {
|
||||
"max_retries": "5",
|
||||
"retry_delay": "5",
|
||||
"heartbeat_interval": "5"
|
||||
}
|
||||
|
||||
# 保存配置
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||
config.write(f)
|
||||
|
||||
logger.info(f"已创建默认配置文件: {CONFIG_FILE}")
|
||||
|
||||
async def connect(self):
|
||||
"""连接到服务器"""
|
||||
logger.info(f"正在连接到服务器: {self.server_url}")
|
||||
|
||||
ssl_context = None
|
||||
if self.server_url.startswith("wss://"):
|
||||
ssl_context = ssl.create_default_context()
|
||||
if self.config.get("Server", "verify_ssl").lower() == "false":
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
try:
|
||||
self.ws = await websockets.connect(
|
||||
self.server_url,
|
||||
ssl=ssl_context,
|
||||
max_size=10 * 1024 * 1024, # 10MB
|
||||
ping_interval=None, # 禁用自动ping,我们将使用自己的心跳
|
||||
close_timeout=5
|
||||
)
|
||||
|
||||
# 等待认证
|
||||
await self.authenticate()
|
||||
|
||||
logger.info("已连接到服务器")
|
||||
self.connection_attempts = 0 # 重置连接尝试次数
|
||||
self.last_heartbeat_time = time.time()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"连接服务器失败: {e}")
|
||||
return False
|
||||
|
||||
async def connect_with_retry(self):
|
||||
"""带重试机制的连接函数"""
|
||||
if self.reconnecting:
|
||||
logger.info("已有重连过程在进行中,跳过")
|
||||
return False
|
||||
|
||||
self.reconnecting = True
|
||||
retry_count = 0
|
||||
|
||||
try:
|
||||
while retry_count < self.max_retries and not self.ws:
|
||||
try:
|
||||
logger.info(f"尝试连接服务器 (尝试 {retry_count + 1}/{self.max_retries})...")
|
||||
success = await self.connect()
|
||||
if success:
|
||||
self.reconnecting = False
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"连接服务器失败: {e}")
|
||||
|
||||
retry_count += 1
|
||||
retry_delay = min(60, self.retry_delay * (2 ** retry_count)) # 指数退避策略
|
||||
logger.info(f"等待 {retry_delay} 秒后重试...")
|
||||
await asyncio.sleep(retry_delay)
|
||||
|
||||
if not self.ws:
|
||||
logger.error(f"达到最大重试次数 ({self.max_retries}),连接失败")
|
||||
self.reconnecting = False
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"重连过程中发生错误: {e}")
|
||||
self.reconnecting = False
|
||||
return False
|
||||
|
||||
self.reconnecting = False
|
||||
return True
|
||||
|
||||
async def authenticate(self):
|
||||
"""客户端认证"""
|
||||
try:
|
||||
# 等待认证挑战
|
||||
challenge_raw = await self.ws.recv()
|
||||
challenge = json.loads(challenge_raw)
|
||||
|
||||
if challenge.get("type") != "auth_challenge":
|
||||
raise ValueError("无效的认证挑战")
|
||||
|
||||
# 收集系统信息
|
||||
system_info = self.get_system_info()
|
||||
|
||||
# 发送认证响应
|
||||
await self.ws.send(json.dumps({
|
||||
"type": "auth_response",
|
||||
"response": f"client_{random.getrandbits(32)}",
|
||||
"client_info": {
|
||||
"version": "1.0.1", # 更新版本号
|
||||
"os": "Windows",
|
||||
"screen_resolution": self.get_screen_resolution(),
|
||||
"system_info": system_info
|
||||
}
|
||||
}))
|
||||
|
||||
# 等待认证结果
|
||||
result_raw = await self.ws.recv()
|
||||
result = json.loads(result_raw)
|
||||
|
||||
if result.get("type") != "auth_result" or result.get("status") != "success":
|
||||
raise ValueError("认证失败")
|
||||
|
||||
# 保存客户端ID
|
||||
self.client_id = result.get("client_id")
|
||||
logger.info(f"认证成功,客户端ID: {self.client_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"认证失败: {e}")
|
||||
raise
|
||||
|
||||
def get_system_info(self):
|
||||
"""获取系统信息"""
|
||||
system_info = {}
|
||||
try:
|
||||
system_info["hostname"] = os.environ.get("COMPUTERNAME", "Unknown")
|
||||
system_info["username"] = os.environ.get("USERNAME", "Unknown")
|
||||
system_info["processor"] = os.environ.get("PROCESSOR_IDENTIFIER", "Unknown")
|
||||
|
||||
# 获取系统内存信息
|
||||
mem = ctypes.c_ulonglong()
|
||||
ctypes.windll.kernel32.GetPhysicallyInstalledSystemMemory(ctypes.byref(mem))
|
||||
system_info["memory_gb"] = round(mem.value / (1024 * 1024), 2)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取系统信息失败: {e}")
|
||||
|
||||
return system_info
|
||||
|
||||
def get_screen_resolution(self):
|
||||
"""获取屏幕分辨率"""
|
||||
user32 = ctypes.windll.user32
|
||||
return [user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)]
|
||||
|
||||
def get_game_window(self):
|
||||
"""获取游戏窗口句柄"""
|
||||
window_title = self.config.get("Game", "window_title")
|
||||
hwnd = win32gui.FindWindow(None, window_title)
|
||||
if hwnd == 0:
|
||||
logger.warning(f"找不到游戏窗口: {window_title}")
|
||||
return None
|
||||
return hwnd
|
||||
|
||||
def capture_game_screen(self, hwnd=None):
|
||||
"""截取游戏画面"""
|
||||
try:
|
||||
if hwnd is None:
|
||||
hwnd = self.get_game_window()
|
||||
if hwnd is None:
|
||||
return None
|
||||
|
||||
# 获取窗口位置和大小
|
||||
rect = win32gui.GetWindowRect(hwnd)
|
||||
x, y, width, height = rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1]
|
||||
|
||||
# 检查窗口是否最小化
|
||||
if width <= 0 or height <= 0:
|
||||
logger.warning("游戏窗口被最小化或大小无效")
|
||||
return None
|
||||
|
||||
# 使用mss进行截图(如果可用),速度比PIL快5-10倍
|
||||
if MSS_AVAILABLE:
|
||||
with mss.mss() as sct:
|
||||
monitor = {"top": y, "left": x, "width": width, "height": height}
|
||||
sct_img = sct.grab(monitor)
|
||||
# 将mss图像转换为PIL图像
|
||||
img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
|
||||
else:
|
||||
# 使用PIL进行截图(备选方案)
|
||||
img = ImageGrab.grab(bbox=(x, y, x + width, y + height))
|
||||
|
||||
# 压缩图像
|
||||
quality = int(self.config.get("Capture", "quality"))
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="JPEG", quality=quality)
|
||||
|
||||
# 转换为Base64
|
||||
img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
|
||||
return {
|
||||
"image": img_base64,
|
||||
"window_rect": [x, y, width, height],
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"截取游戏画面失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
async def send_heartbeat(self):
|
||||
"""发送心跳包"""
|
||||
if not self.ws or not self.running:
|
||||
return False
|
||||
|
||||
try:
|
||||
await self.ws.send(json.dumps({
|
||||
"type": "heartbeat",
|
||||
"timestamp": time.time(),
|
||||
"client_id": self.client_id,
|
||||
"game_state": {
|
||||
"in_battle": self.game_state["in_battle"],
|
||||
"current_map": self.game_state["current_map"]
|
||||
}
|
||||
}))
|
||||
|
||||
# 等待心跳响应
|
||||
response_raw = await asyncio.wait_for(
|
||||
self.ws.recv(),
|
||||
timeout=5.0
|
||||
)
|
||||
|
||||
# 检查响应
|
||||
response = json.loads(response_raw)
|
||||
if response.get("type") != "heartbeat_response":
|
||||
logger.warning(f"收到非心跳响应: {response.get('type')}")
|
||||
return False
|
||||
|
||||
self.last_heartbeat_time = time.time()
|
||||
return True
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("心跳超时")
|
||||
return False
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.warning("发送心跳时连接已关闭")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"发送心跳失败: {e}")
|
||||
return False
|
||||
|
||||
async def heartbeat_loop(self):
|
||||
"""心跳循环"""
|
||||
heartbeat_interval = float(self.config.get("Connection", "heartbeat_interval", fallback="5"))
|
||||
|
||||
while self.running:
|
||||
if self.ws and not self.ws.closed:
|
||||
success = await self.send_heartbeat()
|
||||
if not success:
|
||||
# 心跳失败,检查连接状态
|
||||
if time.time() - self.last_heartbeat_time > heartbeat_interval * 3:
|
||||
logger.warning(f"心跳超时超过 {heartbeat_interval * 3} 秒,尝试重新连接")
|
||||
if self.ws:
|
||||
await self.ws.close()
|
||||
self.ws = None
|
||||
|
||||
await asyncio.sleep(heartbeat_interval)
|
||||
|
||||
async def reconnect_loop(self):
|
||||
"""重连检查循环"""
|
||||
while self.running:
|
||||
if not self.ws or self.ws.closed:
|
||||
logger.warning("WebSocket连接已断开,尝试重连...")
|
||||
if await self.connect_with_retry():
|
||||
logger.info("重连成功")
|
||||
else:
|
||||
logger.error("重连失败")
|
||||
# 不要立即停止,继续尝试
|
||||
|
||||
await asyncio.sleep(5) # 每5秒检查一次连接状态
|
||||
|
||||
async def execute_action(self, action):
|
||||
"""执行操作"""
|
||||
try:
|
||||
action_type = action.get("type")
|
||||
|
||||
# 等待指定的延迟时间
|
||||
if "delay" in action:
|
||||
await asyncio.sleep(action["delay"])
|
||||
|
||||
# 获取游戏窗口
|
||||
hwnd = self.get_game_window()
|
||||
if hwnd is None:
|
||||
return
|
||||
|
||||
# 确保窗口处于前台
|
||||
if win32gui.GetForegroundWindow() != hwnd:
|
||||
try:
|
||||
# 尝试多种方法激活窗口
|
||||
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) # 恢复窗口(如果最小化)
|
||||
win32gui.SetForegroundWindow(hwnd) # 尝试置为前台
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 如果窗口仍然不在前台,使用更强的方法
|
||||
if win32gui.GetForegroundWindow() != hwnd:
|
||||
# 获取当前激活窗口的线程和进程ID
|
||||
curr_hwnd = win32gui.GetForegroundWindow()
|
||||
curr_thread_id = win32process.GetWindowThreadProcessId(curr_hwnd)[0]
|
||||
# 获取目标窗口的线程和进程ID
|
||||
target_thread_id = win32process.GetWindowThreadProcessId(hwnd)[0]
|
||||
# 附加线程输入
|
||||
win32process.AttachThreadInput(target_thread_id, curr_thread_id, True)
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
win32gui.BringWindowToTop(hwnd)
|
||||
win32process.AttachThreadInput(target_thread_id, curr_thread_id, False)
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"激活窗口失败: {e}")
|
||||
|
||||
# 获取窗口位置
|
||||
rect = win32gui.GetWindowRect(hwnd)
|
||||
window_x, window_y = rect[0], rect[1]
|
||||
|
||||
# 执行不同类型的操作
|
||||
if action_type == "move_to":
|
||||
# 移动到指定位置
|
||||
position = action.get("position", [0, 0])
|
||||
x, y = position[0] + window_x, position[1] + window_y
|
||||
|
||||
# 生成人类化的移动路径
|
||||
current_pos = win32gui.GetCursorPos()
|
||||
path = self.generate_movement_path(current_pos, [x, y])
|
||||
|
||||
# 执行移动
|
||||
for point in path:
|
||||
win32api.SetCursorPos((int(point[0]), int(point[1])))
|
||||
await asyncio.sleep(0.01) # 10ms延迟
|
||||
|
||||
elif action_type == "click":
|
||||
# 点击指定位置
|
||||
position = action.get("position", [0, 0])
|
||||
x, y = position[0] + window_x, position[1] + window_y
|
||||
|
||||
# 移动到位置
|
||||
current_pos = win32gui.GetCursorPos()
|
||||
path = self.generate_movement_path(current_pos, [x, y])
|
||||
|
||||
for point in path:
|
||||
win32api.SetCursorPos((int(point[0]), int(point[1])))
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
# 执行点击
|
||||
mouse.click()
|
||||
|
||||
elif action_type == "use_skill":
|
||||
# 使用技能
|
||||
key = action.get("key", "1")
|
||||
keyboard.press(key)
|
||||
await asyncio.sleep(0.05)
|
||||
keyboard.release(key)
|
||||
|
||||
# 如果有目标位置,移动鼠标并点击
|
||||
if "target_position" in action:
|
||||
pos = action["target_position"]
|
||||
x, y = pos[0] + window_x, pos[1] + window_y
|
||||
win32api.SetCursorPos((int(x), int(y)))
|
||||
await asyncio.sleep(0.05)
|
||||
mouse.click()
|
||||
|
||||
elif action_type == "interact":
|
||||
# 交互
|
||||
key = action.get("key", "f")
|
||||
keyboard.press(key)
|
||||
await asyncio.sleep(0.1)
|
||||
keyboard.release(key)
|
||||
|
||||
elif action_type == "move_random":
|
||||
# 随机移动
|
||||
direction = action.get("direction", "right")
|
||||
duration = action.get("duration", 1.0)
|
||||
|
||||
# 方向键映射
|
||||
dir_keys = {
|
||||
"up": "w",
|
||||
"down": "s",
|
||||
"left": "a",
|
||||
"right": "d"
|
||||
}
|
||||
|
||||
key = dir_keys.get(direction, "d")
|
||||
keyboard.press(key)
|
||||
await asyncio.sleep(duration)
|
||||
keyboard.release(key)
|
||||
|
||||
elif action_type == "use_item":
|
||||
# 使用物品
|
||||
key = action.get("key", "f1")
|
||||
keyboard.press(key)
|
||||
await asyncio.sleep(0.1)
|
||||
keyboard.release(key)
|
||||
|
||||
elif action_type == "stop":
|
||||
# 停止所有按键
|
||||
for key in ["w", "a", "s", "d", "1", "2", "3", "4", "5", "6"]:
|
||||
if keyboard.is_pressed(key):
|
||||
keyboard.release(key)
|
||||
|
||||
elif action_type == "type_text":
|
||||
# 输入文本
|
||||
text = action.get("text", "")
|
||||
if text:
|
||||
keyboard.write(text, delay=0.05)
|
||||
|
||||
elif action_type == "press_key_combo":
|
||||
# 按下组合键
|
||||
keys = action.get("keys", [])
|
||||
if keys:
|
||||
for key in keys:
|
||||
keyboard.press(key)
|
||||
await asyncio.sleep(0.1)
|
||||
for key in reversed(keys):
|
||||
keyboard.release(key)
|
||||
|
||||
else:
|
||||
logger.warning(f"未知操作类型: {action_type}")
|
||||
|
||||
# 更新游戏状态
|
||||
self.game_state["last_operation_time"] = time.time()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"执行操作失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
def generate_movement_path(self, start_pos, end_pos, steps=None):
|
||||
"""生成模拟人类的鼠标移动路径"""
|
||||
# 计算距离
|
||||
distance = ((end_pos[0] - start_pos[0])**2 + (end_pos[1] - start_pos[1])**2)**0.5
|
||||
|
||||
# 如果未指定步数,根据距离计算
|
||||
if steps is None:
|
||||
steps = max(int(distance / 20), 5) # 每20像素一个点,至少5个点
|
||||
|
||||
# 生成基础路径
|
||||
t = np.linspace(0, 1, steps)
|
||||
path = []
|
||||
|
||||
# 动态调整平滑度
|
||||
smoothness = float(self.config.get("Game", "movement_smoothness", fallback="0.8"))
|
||||
|
||||
for i in range(steps):
|
||||
# 基础线性插值
|
||||
x = start_pos[0] + (end_pos[0] - start_pos[0]) * t[i]
|
||||
y = start_pos[1] + (end_pos[1] - start_pos[1]) * t[i]
|
||||
|
||||
# 添加随机偏移(越靠近中间偏移越大)
|
||||
mid_factor = 4 * t[i] * (1 - t[i]) # 在中间最大
|
||||
max_offset = distance * 0.05 * mid_factor * (1 - smoothness) # 最大偏移为距离的5%,受平滑度影响
|
||||
|
||||
offset_x = random.normalvariate(0, max_offset / 3)
|
||||
offset_y = random.normalvariate(0, max_offset / 3)
|
||||
|
||||
# 添加到路径
|
||||
path.append([x + offset_x, y + offset_y])
|
||||
|
||||
# 确保起点和终点准确
|
||||
path[0] = start_pos
|
||||
path[-1] = end_pos
|
||||
|
||||
return path
|
||||
|
||||
def analyze_image(self, image_data, detection_results):
|
||||
"""
|
||||
分析图像数据,更新游戏状态
|
||||
|
||||
参数:
|
||||
image_data (dict): 图像数据
|
||||
detection_results (list): 检测结果
|
||||
"""
|
||||
# 更新战斗状态
|
||||
monsters_detected = False
|
||||
for det in detection_results:
|
||||
if det["class_name"] in ["monster", "boss"]:
|
||||
monsters_detected = True
|
||||
break
|
||||
|
||||
self.game_state["in_battle"] = monsters_detected
|
||||
|
||||
# 分析血条和蓝条
|
||||
hp_bars = [d for d in detection_results if d["class_name"] == "hp_bar"]
|
||||
mp_bars = [d for d in detection_results if d["class_name"] == "mp_bar"]
|
||||
|
||||
if hp_bars:
|
||||
# 估算血量百分比
|
||||
self.game_state["hp_percent"] = self.estimate_bar_percent(hp_bars[0])
|
||||
|
||||
if mp_bars:
|
||||
# 估算蓝量百分比
|
||||
self.game_state["mp_percent"] = self.estimate_bar_percent(mp_bars[0])
|
||||
|
||||
# 检测技能冷却
|
||||
cooldowns = [d for d in detection_results if d["class_name"] == "cooldown"]
|
||||
self.game_state["cooldowns"] = {}
|
||||
|
||||
for cd in cooldowns:
|
||||
if "skill_id" in cd:
|
||||
self.game_state["cooldowns"][cd["skill_id"]] = cd.get("remaining_time", 1.0)
|
||||
|
||||
def estimate_bar_percent(self, bar_detection):
|
||||
"""
|
||||
估计血条/蓝条的百分比
|
||||
|
||||
参数:
|
||||
bar_detection (dict): 条形检测结果
|
||||
|
||||
返回:
|
||||
float: 百分比值(0-100)
|
||||
"""
|
||||
# 这里需要根据实际情况实现
|
||||
# 临时方案:返回检测结果中的值,如果没有则返回默认值
|
||||
return bar_detection.get("percent", 100)
|
||||
|
||||
async def capture_and_process_loop(self):
|
||||
"""截图和处理循环"""
|
||||
consecutive_errors = 0
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# 检查WebSocket连接
|
||||
if not self.ws or self.ws.closed:
|
||||
await asyncio.sleep(0.5) # 连接断开时等待
|
||||
continue
|
||||
|
||||
# 获取游戏窗口
|
||||
hwnd = self.get_game_window()
|
||||
if hwnd is None:
|
||||
await asyncio.sleep(1.0) # 找不到窗口时等待1秒
|
||||
continue
|
||||
|
||||
# 截取游戏画面
|
||||
screen_data = self.capture_game_screen(hwnd)
|
||||
if screen_data is None:
|
||||
await asyncio.sleep(0.5) # 截图失败时等待0.5秒
|
||||
continue
|
||||
|
||||
# 准备请求数据
|
||||
request = {
|
||||
"type": "image",
|
||||
"request_id": f"req_{random.getrandbits(32)}",
|
||||
"timestamp": time.time(),
|
||||
"data": screen_data["image"],
|
||||
"game_state": self.game_state,
|
||||
"window_rect": screen_data["window_rect"]
|
||||
}
|
||||
|
||||
# 发送请求
|
||||
await self.ws.send(json.dumps(request))
|
||||
|
||||
# 等待响应
|
||||
response_raw = await asyncio.wait_for(
|
||||
self.ws.recv(),
|
||||
timeout=5.0
|
||||
)
|
||||
|
||||
# 处理响应
|
||||
response = json.loads(response_raw)
|
||||
|
||||
if response.get("type") == "action_response":
|
||||
# 获取检测结果并更新游戏状态
|
||||
if "detections" in response:
|
||||
self.analyze_image(screen_data, response["detections"])
|
||||
|
||||
# 执行动作
|
||||
actions = response.get("actions", [])
|
||||
|
||||
# 按优先级排序
|
||||
actions.sort(key=lambda x: x.get("execution_priority", 1.0))
|
||||
|
||||
for action in actions:
|
||||
await self.execute_action(action)
|
||||
|
||||
# 重置错误计数
|
||||
consecutive_errors = 0
|
||||
|
||||
elif response.get("type") == "error":
|
||||
logger.error(f"服务器返回错误: {response.get('message')}")
|
||||
consecutive_errors += 1
|
||||
|
||||
# 等待指定的间隔时间
|
||||
await asyncio.sleep(self.capture_interval)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("等待服务器响应超时")
|
||||
consecutive_errors += 1
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.error("WebSocket连接已关闭")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"处理循环出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
consecutive_errors += 1
|
||||
await asyncio.sleep(1.0) # 出错时等待1秒
|
||||
|
||||
# 如果连续错误过多,尝试重新连接
|
||||
if consecutive_errors >= 5:
|
||||
logger.warning(f"连续出错 {consecutive_errors} 次,尝试重新连接")
|
||||
if self.ws:
|
||||
await self.ws.close()
|
||||
self.ws = None
|
||||
consecutive_errors = 0
|
||||
|
||||
async def run(self):
|
||||
"""运行客户端"""
|
||||
self.running = True
|
||||
|
||||
# 连接到服务器
|
||||
if not await self.connect_with_retry():
|
||||
self.running = False
|
||||
return
|
||||
|
||||
try:
|
||||
# 创建任务
|
||||
capture_task = asyncio.create_task(self.capture_and_process_loop())
|
||||
heartbeat_task = asyncio.create_task(self.heartbeat_loop())
|
||||
reconnect_task = asyncio.create_task(self.reconnect_loop())
|
||||
|
||||
# 等待任务完成
|
||||
await asyncio.gather(capture_task, heartbeat_task, reconnect_task)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("客户端任务已取消")
|
||||
except Exception as e:
|
||||
logger.error(f"客户端运行出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
finally:
|
||||
self.running = False
|
||||
if self.ws:
|
||||
await self.ws.close()
|
||||
logger.info("客户端已停止")
|
||||
|
||||
def start(self):
|
||||
"""启动客户端"""
|
||||
try:
|
||||
# 启动心跳监控线程(备用方案,以防异步心跳失效)
|
||||
self._monitor_thread = threading.Thread(target=self._monitor_connection)
|
||||
self._monitor_thread.daemon = True
|
||||
self._monitor_thread.start()
|
||||
|
||||
# 运行主循环
|
||||
asyncio.run(self.run())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("用户中断,正在退出...")
|
||||
except Exception as e:
|
||||
logger.error(f"客户端出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
def _monitor_connection(self):
|
||||
"""监控连接的后台线程"""
|
||||
while True:
|
||||
try:
|
||||
time.sleep(30) # 每30秒检查一次
|
||||
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
# 检查心跳时间
|
||||
if self.last_heartbeat_time > 0 and time.time() - self.last_heartbeat_time > 60:
|
||||
logger.warning("心跳超时,可能需要重连")
|
||||
# 不直接重连,留给重连循环处理
|
||||
except Exception as e:
|
||||
logger.error(f"连接监控线程出错: {e}")
|
||||
|
||||
def stop(self):
|
||||
"""停止客户端"""
|
||||
self.running = False
|
||||
logger.info("正在停止客户端...")
|
||||
|
||||
# 创建默认配置文件(如果不存在)
|
||||
def ensure_config():
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
config["Server"] = {
|
||||
"url": "wss://your-server-url:8080/ws",
|
||||
"verify_ssl": "false"
|
||||
}
|
||||
|
||||
config["Capture"] = {
|
||||
"interval": "0.5",
|
||||
"quality": "70"
|
||||
}
|
||||
|
||||
config["Game"] = {
|
||||
"window_title": "地下城与勇士",
|
||||
"key_mapping": "default",
|
||||
"movement_smoothness": "0.8"
|
||||
}
|
||||
|
||||
config["Connection"] = {
|
||||
"max_retries": "5",
|
||||
"retry_delay": "5",
|
||||
"heartbeat_interval": "5"
|
||||
}
|
||||
|
||||
# 保存配置
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||
config.write(f)
|
||||
|
||||
print(f"已创建默认配置文件: {CONFIG_FILE}")
|
||||
print("请编辑配置文件设置正确的服务器地址等信息")
|
||||
|
||||
# 启动客户端
|
||||
if __name__ == "__main__":
|
||||
# 确保配置文件存在
|
||||
ensure_config()
|
||||
|
||||
try:
|
||||
client = DNFAutoClient()
|
||||
client.start()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("用户中断,正在退出...")
|
||||
except Exception as e:
|
||||
logger.error(f"客户端出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
@@ -1,17 +0,0 @@
|
||||
[Server]
|
||||
url = wss://your-server-ip:8080/ws
|
||||
verify_ssl = false
|
||||
|
||||
[Capture]
|
||||
interval = 0.5
|
||||
quality = 70
|
||||
|
||||
[Game]
|
||||
window_title = 地下城与勇士
|
||||
key_mapping = default
|
||||
movement_smoothness = 0.8
|
||||
|
||||
[Connection]
|
||||
max_retries = 5
|
||||
retry_delay = 5
|
||||
heartbeat_interval = 5
|
||||
@@ -1,4 +0,0 @@
|
||||
@echo off
|
||||
echo 正在启动DNF自动化客户端...
|
||||
python client.py
|
||||
pause
|
||||
0
models/__init__.py
Normal file
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 220 KiB |
@@ -99,3 +99,100 @@
|
||||
97, 0.0092242, 0.0074598, 0.0032303, 0.98604, 0.98365, 0.99366, 0.9308, 0.0082706, 0.005929, 0.0022603, 0.000496, 0.000496, 0.000496
|
||||
98, 0.0088322, 0.007461, 0.0025581, 0.9797, 0.9832, 0.99359, 0.92623, 0.0086755, 0.006093, 0.0023018, 0.000397, 0.000397, 0.000397
|
||||
99, 0.009008, 0.0076601, 0.0027773, 0.97461, 0.98322, 0.99354, 0.93797, 0.0082771, 0.0060029, 0.0023297, 0.000298, 0.000298, 0.000298
|
||||
0, 0.10537, 0.076466, 0.071107, 0.0057694, 0.53319, 0.039631, 0.0090939, 0.083053, 0.030588, 0.065088, 0.070833, 0.0032407, 0.0032407
|
||||
1, 0.084226, 0.072698, 0.061562, 0.39192, 0.37511, 0.099785, 0.024765, 0.078095, 0.027365, 0.054508, 0.040768, 0.006509, 0.006509
|
||||
2, 0.081015, 0.064917, 0.053284, 0.45499, 0.44536, 0.25051, 0.085181, 0.076592, 0.021748, 0.044283, 0.010637, 0.0097112, 0.0097112
|
||||
3, 0.074563, 0.061782, 0.048053, 0.73241, 0.43019, 0.47748, 0.18303, 0.062812, 0.018912, 0.037823, 0.009703, 0.009703, 0.009703
|
||||
4, 0.068283, 0.057336, 0.042833, 0.67085, 0.54173, 0.50967, 0.22036, 0.054194, 0.01771, 0.0334, 0.009703, 0.009703, 0.009703
|
||||
5, 0.061134, 0.053866, 0.037512, 0.65146, 0.45436, 0.555, 0.26817, 0.052837, 0.016332, 0.028931, 0.009604, 0.009604, 0.009604
|
||||
6, 0.05909, 0.053523, 0.033439, 0.57689, 0.67196, 0.67603, 0.32589, 0.047249, 0.014892, 0.025372, 0.009505, 0.009505, 0.009505
|
||||
7, 0.056171, 0.05248, 0.031248, 0.79275, 0.60855, 0.73263, 0.391, 0.045996, 0.015203, 0.021998, 0.009406, 0.009406, 0.009406
|
||||
8, 0.054112, 0.048681, 0.027633, 0.88722, 0.72512, 0.76046, 0.38817, 0.042352, 0.014402, 0.01973, 0.009307, 0.009307, 0.009307
|
||||
9, 0.052162, 0.048991, 0.025344, 0.89922, 0.72099, 0.82979, 0.40421, 0.042111, 0.013725, 0.017555, 0.009208, 0.009208, 0.009208
|
||||
10, 0.0513, 0.048124, 0.024098, 0.9543, 0.73898, 0.87995, 0.53812, 0.035614, 0.013525, 0.015445, 0.009109, 0.009109, 0.009109
|
||||
11, 0.049729, 0.046288, 0.022249, 0.80022, 0.75284, 0.81464, 0.46789, 0.039512, 0.01382, 0.014261, 0.00901, 0.00901, 0.00901
|
||||
12, 0.047242, 0.046023, 0.020322, 0.87033, 0.82, 0.87714, 0.53135, 0.036358, 0.013075, 0.013154, 0.008911, 0.008911, 0.008911
|
||||
13, 0.045947, 0.049008, 0.019099, 0.79432, 0.81413, 0.85648, 0.54684, 0.036657, 0.013149, 0.011747, 0.008812, 0.008812, 0.008812
|
||||
14, 0.044563, 0.045478, 0.017354, 0.86486, 0.82851, 0.86331, 0.58533, 0.030859, 0.012629, 0.010733, 0.008713, 0.008713, 0.008713
|
||||
15, 0.044386, 0.045777, 0.016909, 0.87888, 0.84115, 0.88297, 0.5868, 0.030465, 0.012362, 0.010119, 0.008614, 0.008614, 0.008614
|
||||
16, 0.044421, 0.044363, 0.015621, 0.93144, 0.82596, 0.88851, 0.60501, 0.031073, 0.012311, 0.0097153, 0.008515, 0.008515, 0.008515
|
||||
17, 0.042487, 0.046041, 0.015011, 0.92876, 0.83356, 0.89565, 0.55739, 0.031302, 0.012821, 0.0089586, 0.008416, 0.008416, 0.008416
|
||||
18, 0.042481, 0.041832, 0.014583, 0.84688, 0.87297, 0.88565, 0.57654, 0.032206, 0.012529, 0.008568, 0.008317, 0.008317, 0.008317
|
||||
19, 0.041834, 0.041992, 0.014201, 0.8425, 0.87675, 0.87723, 0.56809, 0.035273, 0.012553, 0.0082376, 0.008218, 0.008218, 0.008218
|
||||
20, 0.041529, 0.040971, 0.013827, 0.88358, 0.89188, 0.90811, 0.61107, 0.029281, 0.012575, 0.0083256, 0.008119, 0.008119, 0.008119
|
||||
21, 0.039979, 0.041675, 0.013442, 0.90751, 0.87639, 0.88663, 0.65531, 0.028874, 0.011992, 0.0077705, 0.00802, 0.00802, 0.00802
|
||||
22, 0.040631, 0.042681, 0.013149, 0.92106, 0.89742, 0.90632, 0.63937, 0.027697, 0.012193, 0.0073721, 0.007921, 0.007921, 0.007921
|
||||
23, 0.039521, 0.041223, 0.012621, 0.89472, 0.8844, 0.909, 0.59444, 0.029174, 0.012005, 0.0069702, 0.007822, 0.007822, 0.007822
|
||||
24, 0.039209, 0.040794, 0.012326, 0.94475, 0.8852, 0.91868, 0.64485, 0.029654, 0.012124, 0.0069315, 0.007723, 0.007723, 0.007723
|
||||
25, 0.038706, 0.042495, 0.012114, 0.94535, 0.88812, 0.91316, 0.66164, 0.025027, 0.011684, 0.0064895, 0.007624, 0.007624, 0.007624
|
||||
26, 0.038853, 0.039949, 0.011693, 0.93719, 0.88993, 0.91549, 0.64189, 0.031298, 0.011859, 0.0065789, 0.007525, 0.007525, 0.007525
|
||||
27, 0.037325, 0.038238, 0.011581, 0.94332, 0.8866, 0.91919, 0.69192, 0.024287, 0.011155, 0.0062871, 0.007426, 0.007426, 0.007426
|
||||
28, 0.037054, 0.039379, 0.010862, 0.94197, 0.88904, 0.91807, 0.67987, 0.027484, 0.011633, 0.0063429, 0.007327, 0.007327, 0.007327
|
||||
29, 0.037804, 0.039336, 0.010948, 0.94615, 0.88732, 0.92419, 0.71138, 0.024992, 0.011194, 0.0060732, 0.007228, 0.007228, 0.007228
|
||||
30, 0.035318, 0.038011, 0.010588, 0.93566, 0.90238, 0.9254, 0.69709, 0.025254, 0.01115, 0.0060346, 0.007129, 0.007129, 0.007129
|
||||
31, 0.035848, 0.040036, 0.010657, 0.95219, 0.87909, 0.91779, 0.68068, 0.02775, 0.011805, 0.0060232, 0.00703, 0.00703, 0.00703
|
||||
32, 0.036455, 0.03784, 0.010277, 0.9521, 0.89118, 0.91821, 0.69482, 0.02398, 0.011316, 0.006109, 0.006931, 0.006931, 0.006931
|
||||
33, 0.035511, 0.040274, 0.01034, 0.95571, 0.89172, 0.91894, 0.67729, 0.025415, 0.011374, 0.0058415, 0.006832, 0.006832, 0.006832
|
||||
34, 0.034818, 0.036288, 0.0098505, 0.96073, 0.90876, 0.92328, 0.73266, 0.022878, 0.010763, 0.0055736, 0.006733, 0.006733, 0.006733
|
||||
35, 0.034432, 0.035868, 0.0099866, 0.94404, 0.89441, 0.90445, 0.6819, 0.02454, 0.010886, 0.0055561, 0.006634, 0.006634, 0.006634
|
||||
36, 0.034021, 0.035818, 0.009415, 0.95426, 0.90633, 0.90901, 0.68886, 0.023201, 0.010798, 0.0054224, 0.006535, 0.006535, 0.006535
|
||||
37, 0.033418, 0.03598, 0.0094498, 0.94152, 0.89777, 0.89953, 0.69698, 0.023835, 0.010837, 0.0053287, 0.006436, 0.006436, 0.006436
|
||||
38, 0.033272, 0.036502, 0.0095944, 0.94272, 0.90222, 0.92314, 0.70942, 0.023785, 0.011131, 0.0052042, 0.006337, 0.006337, 0.006337
|
||||
39, 0.033521, 0.035348, 0.0089882, 0.94528, 0.90316, 0.9228, 0.7345, 0.022518, 0.010903, 0.0052435, 0.006238, 0.006238, 0.006238
|
||||
40, 0.032908, 0.035765, 0.0095038, 0.94537, 0.90443, 0.92117, 0.73349, 0.023005, 0.010825, 0.0052056, 0.006139, 0.006139, 0.006139
|
||||
41, 0.031944, 0.03523, 0.0090051, 0.94376, 0.89426, 0.92003, 0.72121, 0.021874, 0.010633, 0.0051577, 0.00604, 0.00604, 0.00604
|
||||
42, 0.032284, 0.034942, 0.0085232, 0.94648, 0.89959, 0.92236, 0.72606, 0.021799, 0.010527, 0.0051405, 0.005941, 0.005941, 0.005941
|
||||
43, 0.03217, 0.035902, 0.0087585, 0.9491, 0.9019, 0.92087, 0.73303, 0.021869, 0.01047, 0.0051244, 0.005842, 0.005842, 0.005842
|
||||
44, 0.031443, 0.033035, 0.0085704, 0.94745, 0.90156, 0.92477, 0.7546, 0.020727, 0.01034, 0.0049707, 0.005743, 0.005743, 0.005743
|
||||
45, 0.031114, 0.036149, 0.0084183, 0.94662, 0.90115, 0.92202, 0.72985, 0.021511, 0.010381, 0.0051055, 0.005644, 0.005644, 0.005644
|
||||
46, 0.030806, 0.033467, 0.0085668, 0.94773, 0.9066, 0.91584, 0.7378, 0.021743, 0.010383, 0.0049932, 0.005545, 0.005545, 0.005545
|
||||
47, 0.03045, 0.035342, 0.0088827, 0.94675, 0.89267, 0.91217, 0.72865, 0.021152, 0.010576, 0.0050758, 0.005446, 0.005446, 0.005446
|
||||
48, 0.030546, 0.034058, 0.0083615, 0.9474, 0.89399, 0.90736, 0.72605, 0.022204, 0.010774, 0.0049421, 0.005347, 0.005347, 0.005347
|
||||
49, 0.030783, 0.035215, 0.0082594, 0.94012, 0.88191, 0.91291, 0.76995, 0.019367, 0.010101, 0.0049213, 0.005248, 0.005248, 0.005248
|
||||
50, 0.030272, 0.031735, 0.0081784, 0.94596, 0.89436, 0.91247, 0.72832, 0.022859, 0.010223, 0.0047657, 0.005149, 0.005149, 0.005149
|
||||
51, 0.030048, 0.033497, 0.0078879, 0.9442, 0.90128, 0.91924, 0.77297, 0.018489, 0.0096083, 0.0046946, 0.00505, 0.00505, 0.00505
|
||||
52, 0.029478, 0.034815, 0.0079617, 0.94805, 0.90482, 0.91374, 0.73906, 0.021018, 0.010099, 0.0047801, 0.004951, 0.004951, 0.004951
|
||||
53, 0.029622, 0.033132, 0.0073636, 0.94613, 0.90688, 0.90788, 0.76831, 0.018532, 0.0098147, 0.0046803, 0.004852, 0.004852, 0.004852
|
||||
54, 0.029849, 0.034944, 0.0074608, 0.95061, 0.90497, 0.90431, 0.73547, 0.020199, 0.010039, 0.0046062, 0.004753, 0.004753, 0.004753
|
||||
55, 0.028862, 0.030966, 0.0074558, 0.95399, 0.90448, 0.92083, 0.77247, 0.017624, 0.0094819, 0.0045197, 0.004654, 0.004654, 0.004654
|
||||
56, 0.028374, 0.031805, 0.0075731, 0.94923, 0.9021, 0.91864, 0.7448, 0.021008, 0.0099044, 0.0046206, 0.004555, 0.004555, 0.004555
|
||||
57, 0.028933, 0.033019, 0.0077383, 0.94261, 0.90025, 0.91444, 0.76969, 0.018055, 0.0092425, 0.0044035, 0.004456, 0.004456, 0.004456
|
||||
58, 0.02764, 0.031741, 0.0071853, 0.9316, 0.90703, 0.90207, 0.7154, 0.019898, 0.0099125, 0.0043523, 0.004357, 0.004357, 0.004357
|
||||
59, 0.027372, 0.032124, 0.0071181, 0.93235, 0.90338, 0.91095, 0.78268, 0.017186, 0.0096073, 0.0043241, 0.004258, 0.004258, 0.004258
|
||||
60, 0.027831, 0.03038, 0.0073338, 0.93878, 0.90209, 0.91096, 0.74039, 0.02007, 0.0099401, 0.0042286, 0.004159, 0.004159, 0.004159
|
||||
61, 0.028398, 0.031714, 0.0072932, 0.9362, 0.90344, 0.92164, 0.79353, 0.017023, 0.0093412, 0.0041499, 0.00406, 0.00406, 0.00406
|
||||
62, 0.02799, 0.034212, 0.0075848, 0.92984, 0.90388, 0.90626, 0.74251, 0.01955, 0.0094862, 0.0041463, 0.003961, 0.003961, 0.003961
|
||||
63, 0.026864, 0.029966, 0.0072165, 0.9445, 0.90611, 0.9103, 0.75938, 0.018653, 0.0093565, 0.0041413, 0.003862, 0.003862, 0.003862
|
||||
64, 0.026335, 0.029561, 0.0066826, 0.93036, 0.90621, 0.92241, 0.78571, 0.017073, 0.0095363, 0.0040866, 0.003763, 0.003763, 0.003763
|
||||
65, 0.026865, 0.032126, 0.0069433, 0.93762, 0.8962, 0.91818, 0.76424, 0.019093, 0.0098457, 0.0040423, 0.003664, 0.003664, 0.003664
|
||||
66, 0.025874, 0.029991, 0.0067089, 0.93369, 0.90285, 0.91089, 0.75601, 0.01744, 0.0095415, 0.0040609, 0.003565, 0.003565, 0.003565
|
||||
67, 0.025686, 0.029558, 0.0068254, 0.94118, 0.89702, 0.93001, 0.80512, 0.016593, 0.0091715, 0.0040623, 0.003466, 0.003466, 0.003466
|
||||
68, 0.026146, 0.0306, 0.006672, 0.9435, 0.89793, 0.92328, 0.78872, 0.016968, 0.0094819, 0.0039386, 0.003367, 0.003367, 0.003367
|
||||
69, 0.02575, 0.03149, 0.0066763, 0.95011, 0.89324, 0.92115, 0.77461, 0.017017, 0.00915, 0.0038871, 0.003268, 0.003268, 0.003268
|
||||
70, 0.025257, 0.029951, 0.0062569, 0.94152, 0.90354, 0.93186, 0.79316, 0.016541, 0.0089645, 0.0037884, 0.003169, 0.003169, 0.003169
|
||||
71, 0.025736, 0.031298, 0.0063836, 0.94456, 0.89936, 0.98277, 0.85375, 0.015854, 0.0091391, 0.0037851, 0.00307, 0.00307, 0.00307
|
||||
72, 0.025147, 0.029945, 0.0065308, 0.94031, 0.90261, 0.98337, 0.80994, 0.016953, 0.0095129, 0.0037837, 0.002971, 0.002971, 0.002971
|
||||
73, 0.025429, 0.029684, 0.0067952, 0.9484, 0.89445, 0.97873, 0.85639, 0.015808, 0.0090361, 0.0037594, 0.002872, 0.002872, 0.002872
|
||||
74, 0.0244, 0.02808, 0.006261, 0.9484, 0.89159, 0.96903, 0.8146, 0.01757, 0.0091019, 0.0037436, 0.002773, 0.002773, 0.002773
|
||||
75, 0.024458, 0.029165, 0.0064301, 0.9414, 0.90268, 0.96465, 0.82351, 0.016609, 0.0093188, 0.0037172, 0.002674, 0.002674, 0.002674
|
||||
76, 0.024491, 0.028867, 0.0065167, 0.93885, 0.90952, 0.96421, 0.82762, 0.015425, 0.0091639, 0.0037354, 0.002575, 0.002575, 0.002575
|
||||
77, 0.024234, 0.030084, 0.0061172, 0.92641, 0.92039, 0.97935, 0.84484, 0.015876, 0.0093784, 0.0036685, 0.002476, 0.002476, 0.002476
|
||||
78, 0.023912, 0.027955, 0.0057952, 0.92526, 0.92039, 0.97048, 0.83617, 0.016671, 0.0094657, 0.0036557, 0.002377, 0.002377, 0.002377
|
||||
79, 0.023572, 0.028896, 0.0056073, 0.94601, 0.89761, 0.96783, 0.83554, 0.015535, 0.0090661, 0.0036031, 0.002278, 0.002278, 0.002278
|
||||
80, 0.023433, 0.027542, 0.0060494, 0.95198, 0.89309, 0.96861, 0.83564, 0.01569, 0.0090857, 0.0035759, 0.002179, 0.002179, 0.002179
|
||||
81, 0.023474, 0.029614, 0.0059355, 0.956, 0.89106, 0.96929, 0.83867, 0.015587, 0.0091519, 0.0035584, 0.00208, 0.00208, 0.00208
|
||||
82, 0.022964, 0.028318, 0.0058851, 0.95519, 0.89445, 0.97281, 0.84384, 0.01506, 0.0089855, 0.003513, 0.001981, 0.001981, 0.001981
|
||||
83, 0.023384, 0.028354, 0.0057465, 0.95494, 0.89426, 0.98393, 0.86829, 0.015179, 0.0090079, 0.0034722, 0.001882, 0.001882, 0.001882
|
||||
84, 0.022817, 0.027814, 0.0059098, 0.95244, 0.89233, 0.97627, 0.84317, 0.015875, 0.0092964, 0.0034103, 0.001783, 0.001783, 0.001783
|
||||
85, 0.023342, 0.027297, 0.0060606, 0.95319, 0.89174, 0.97141, 0.84056, 0.015359, 0.0091238, 0.0033967, 0.001684, 0.001684, 0.001684
|
||||
86, 0.022191, 0.027358, 0.005542, 0.9539, 0.88966, 0.97417, 0.85334, 0.015129, 0.0091815, 0.0034193, 0.001585, 0.001585, 0.001585
|
||||
87, 0.022573, 0.027708, 0.0056441, 0.95191, 0.89295, 0.97479, 0.84976, 0.015716, 0.0091705, 0.0033853, 0.001486, 0.001486, 0.001486
|
||||
88, 0.022409, 0.027443, 0.0058222, 0.92751, 0.9028, 0.96398, 0.84426, 0.015372, 0.008944, 0.0033563, 0.001387, 0.001387, 0.001387
|
||||
89, 0.021741, 0.026495, 0.0053773, 0.94905, 0.89763, 0.97, 0.84371, 0.014766, 0.0090199, 0.0033002, 0.001288, 0.001288, 0.001288
|
||||
90, 0.021996, 0.028446, 0.0058893, 0.93917, 0.88982, 0.96402, 0.8402, 0.01475, 0.0089879, 0.0032734, 0.001189, 0.001189, 0.001189
|
||||
91, 0.022016, 0.027665, 0.0055442, 0.94716, 0.90265, 0.97066, 0.849, 0.01469, 0.0089841, 0.0033016, 0.00109, 0.00109, 0.00109
|
||||
92, 0.021633, 0.027603, 0.0055049, 0.9475, 0.90336, 0.96807, 0.84835, 0.014596, 0.0091195, 0.0032879, 0.000991, 0.000991, 0.000991
|
||||
93, 0.021683, 0.026557, 0.0052333, 0.94842, 0.90211, 0.97055, 0.85609, 0.014893, 0.0090179, 0.0032596, 0.000892, 0.000892, 0.000892
|
||||
94, 0.021309, 0.025954, 0.0053442, 0.94614, 0.90127, 0.97183, 0.85953, 0.014493, 0.0088654, 0.0032242, 0.000793, 0.000793, 0.000793
|
||||
95, 0.021511, 0.026043, 0.0054107, 0.94789, 0.90206, 0.96895, 0.86042, 0.014186, 0.008769, 0.003192, 0.000694, 0.000694, 0.000694
|
||||
96, 0.020815, 0.024904, 0.0052537, 0.94738, 0.90358, 0.9677, 0.84651, 0.01453, 0.0088882, 0.0031793, 0.000595, 0.000595, 0.000595
|
||||
|
||||
|
|
Before Width: | Height: | Size: 265 KiB After Width: | Height: | Size: 656 KiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 658 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 594 KiB |
@@ -2,580 +2,467 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
YOLO模型封装,提供图像识别功能
|
||||
优化版 - 增强性能和模型缓存管理
|
||||
修复版 - 解决模型加载依赖问题
|
||||
YOLO模型实现,负责目标检测和识别
|
||||
支持多种YOLO版本:YOLOv5、YOLOv8
|
||||
优化版 - 支持GPU加速和模型优化
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys # 添加缺失的sys模块导入
|
||||
import logging
|
||||
import torch
|
||||
import yaml
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import numpy as np
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import torch
|
||||
import cv2
|
||||
from PIL import Image
|
||||
import statistics
|
||||
from collections import deque
|
||||
|
||||
from config.settings import MODEL, BASE_DIR
|
||||
from config.settings import MODEL
|
||||
|
||||
logger = logging.getLogger("DNFAutoCloud")
|
||||
|
||||
class YOLOModel:
|
||||
"""YOLO模型封装类 - 优化版"""
|
||||
"""YOLO模型类 - 优化版"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化YOLO模型"""
|
||||
self.device = MODEL.get("device", "cuda" if torch.cuda.is_available() else "cpu")
|
||||
self.model = None
|
||||
self.device = MODEL.get("device", "cpu")
|
||||
self.conf_threshold = MODEL.get("conf_threshold", 0.5)
|
||||
self.iou_threshold = MODEL.get("iou_threshold", 0.45)
|
||||
self.img_size = MODEL.get("img_size", 640)
|
||||
self.half_precision = MODEL.get("half_precision", False)
|
||||
self.class_names = self._load_class_names()
|
||||
|
||||
# 性能监控
|
||||
# 性能追踪
|
||||
self.inference_times = deque(maxlen=100)
|
||||
self.batch_size = MODEL.get("batch_size", 1)
|
||||
self.use_half_precision = MODEL.get("half_precision", True) and self.device == "cuda"
|
||||
|
||||
logger.info(f"正在加载YOLO模型: {MODEL.get('name', 'unknown')}")
|
||||
logger.info(f"模型权重路径: {MODEL.get('weights', 'unknown')}")
|
||||
logger.info(f"设备: {self.device}, 半精度: {self.use_half_precision}")
|
||||
|
||||
# 检查权重文件是否存在
|
||||
weights_path = MODEL.get("weights", "")
|
||||
if not os.path.exists(weights_path):
|
||||
# 尝试从备选路径加载
|
||||
alt_paths = [
|
||||
os.path.join(BASE_DIR, "models", "weights", "best.pt"),
|
||||
os.path.join(BASE_DIR, "models", "best.pt"),
|
||||
os.path.join(BASE_DIR, "yolov5", "runs", "train", "exp", "weights", "best.pt")
|
||||
]
|
||||
# 初始化模型
|
||||
self._initialize_model()
|
||||
|
||||
def _load_class_names(self):
|
||||
"""加载类别名称"""
|
||||
try:
|
||||
class_names_path = MODEL.get("class_names", "")
|
||||
if not class_names_path or not os.path.exists(class_names_path):
|
||||
logger.warning(f"类别名称文件不存在: {class_names_path},使用默认类别")
|
||||
return [
|
||||
"monster", "boss", "door", "item", "npc", "player",
|
||||
"hp_bar", "mp_bar", "skill_ready", "cooldown"
|
||||
]
|
||||
|
||||
for path in alt_paths:
|
||||
if os.path.exists(path):
|
||||
weights_path = path
|
||||
logger.info(f"使用备选模型权重: {weights_path}")
|
||||
break
|
||||
# 尝试从YAML加载
|
||||
try:
|
||||
import yaml
|
||||
with open(class_names_path, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
if isinstance(data, dict) and "names" in data:
|
||||
return data["names"]
|
||||
elif "nc" in data and isinstance(data.get("names", []), list):
|
||||
return data["names"]
|
||||
except:
|
||||
# 如果YAML加载失败,尝试直接读取为文本
|
||||
with open(class_names_path, 'r', encoding='utf-8') as f:
|
||||
class_names = [line.strip() for line in f.readlines() if line.strip()]
|
||||
return class_names
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载类别名称时出错: {e}")
|
||||
return [f"class_{i}" for i in range(10)] # 默认类别名
|
||||
|
||||
def _initialize_model(self):
|
||||
"""初始化YOLO模型"""
|
||||
try:
|
||||
# 获取模型路径
|
||||
weights_path = MODEL.get("weights", "")
|
||||
if not weights_path:
|
||||
raise ValueError("未指定模型权重路径")
|
||||
|
||||
if not os.path.exists(weights_path):
|
||||
raise FileNotFoundError(f"找不到模型权重文件: {weights_path}")
|
||||
|
||||
# 加载模型
|
||||
try:
|
||||
if MODEL.get("engine", "") == "onnx":
|
||||
self._load_onnx_model()
|
||||
else:
|
||||
# 尝试不同的加载方法
|
||||
try:
|
||||
self._load_yolov5_custom()
|
||||
except Exception as e:
|
||||
logger.warning(f"使用torch.hub加载YOLOv5模型失败: {e}")
|
||||
logger.info("尝试备选方法加载模型...")
|
||||
self._load_yolov5_manually()
|
||||
raise FileNotFoundError(f"模型权重文件不存在: {weights_path}")
|
||||
|
||||
logger.info("YOLO模型加载成功")
|
||||
logger.info(f"正在加载YOLO模型: {weights_path}")
|
||||
|
||||
# 确定模型引擎类型
|
||||
engine = MODEL.get("engine", "pytorch").lower()
|
||||
|
||||
if engine == "onnx":
|
||||
# 使用ONNX模型
|
||||
if not weights_path.endswith(".onnx"):
|
||||
weights_path = weights_path + ".onnx"
|
||||
|
||||
self._initialize_onnx_model(weights_path)
|
||||
else:
|
||||
# 默认使用PyTorch模型
|
||||
self._initialize_pytorch_model(weights_path)
|
||||
|
||||
logger.info(f"YOLO模型加载成功,运行于 {self.device} 设备")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载YOLO模型失败: {e}")
|
||||
logger.error(f"初始化YOLO模型失败: {e}")
|
||||
raise
|
||||
|
||||
def _load_yolov5_custom(self):
|
||||
"""使用torch.hub加载YOLOv5模型"""
|
||||
def _initialize_pytorch_model(self, weights_path):
|
||||
"""初始化PyTorch YOLO模型"""
|
||||
try:
|
||||
# 尝试从本地加载YOLOv5
|
||||
yolov5_dir = os.path.join(BASE_DIR, "tools", "yolov5")
|
||||
if os.path.exists(yolov5_dir):
|
||||
logger.info(f"从本地目录加载YOLOv5: {yolov5_dir}")
|
||||
sys.path.insert(0, yolov5_dir)
|
||||
|
||||
# 检查设备可用性
|
||||
if self.device.startswith("cuda") and not torch.cuda.is_available():
|
||||
logger.warning("CUDA不可用,切换到CPU模式")
|
||||
self.device = "cpu"
|
||||
self.half_precision = False
|
||||
|
||||
# 设置设备
|
||||
device = torch.device(self.device)
|
||||
|
||||
# 确定YOLO版本并加载
|
||||
model_name = MODEL.get("name", "").lower()
|
||||
|
||||
if "yolov8" in model_name or weights_path.endswith("v8.pt"):
|
||||
# YOLOv8
|
||||
try:
|
||||
# 尝试直接导入YOLOv5模块
|
||||
from models.common import DetectMultiBackend
|
||||
from utils.torch_utils import select_device
|
||||
from utils.general import check_img_size, non_max_suppression, scale_coords
|
||||
from ultralytics import YOLO
|
||||
self.model = YOLO(weights_path)
|
||||
self.model_type = "yolov8"
|
||||
logger.info("已加载YOLOv8模型")
|
||||
except ImportError:
|
||||
logger.error("无法导入ultralytics,请安装: pip install ultralytics")
|
||||
raise
|
||||
else:
|
||||
# YOLOv5(默认)
|
||||
try:
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "../tools/yolov5"))
|
||||
import torch
|
||||
|
||||
# 加载模型
|
||||
self.model = DetectMultiBackend(MODEL.get("weights"), device=self.device)
|
||||
self.stride = self.model.stride
|
||||
self.names = self.model.names
|
||||
self.model = torch.hub.load('ultralytics/yolov5', 'custom',
|
||||
path=weights_path, device=device)
|
||||
|
||||
# 设置参数
|
||||
self.imgsz = check_img_size((640, 640), s=self.stride)
|
||||
self.model.conf = self.conf_threshold
|
||||
self.model.iou = self.iou_threshold
|
||||
self.model.classes = None # 检测所有类别
|
||||
self.model.max_det = 100 # 最大检测数量
|
||||
|
||||
# 使用半精度
|
||||
if self.use_half_precision:
|
||||
# 如果启用半精度且支持
|
||||
if self.half_precision and self.device != "cpu":
|
||||
self.model.half()
|
||||
|
||||
# 预热模型
|
||||
self._warmup_model()
|
||||
self.model_type = "yolov5"
|
||||
logger.info("已加载YOLOv5模型")
|
||||
|
||||
return
|
||||
except ImportError as e:
|
||||
logger.warning(f"无法直接导入YOLOv5模块: {e}")
|
||||
|
||||
# 尝试从torch.hub加载
|
||||
logger.info("尝试从torch.hub加载YOLOv5模型")
|
||||
self.model = torch.hub.load(
|
||||
'ultralytics/yolov5',
|
||||
'custom',
|
||||
path=MODEL.get("weights"),
|
||||
device=self.device,
|
||||
force_reload=True
|
||||
)
|
||||
|
||||
# 设置模型参数
|
||||
self.model.conf = self.conf_threshold
|
||||
self.model.iou = self.iou_threshold
|
||||
|
||||
# 使用半精度
|
||||
if self.use_half_precision:
|
||||
self.model.half()
|
||||
except ImportError:
|
||||
logger.error("无法导入torch或YOLOv5,请确保已正确安装")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"加载YOLOv5模型时出错: {e}")
|
||||
raise
|
||||
|
||||
# 预热模型
|
||||
self._warmup_model()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载YOLOv5模型失败: {e}")
|
||||
logger.error(f"初始化PyTorch模型失败: {e}")
|
||||
raise
|
||||
|
||||
def _load_yolov5_manually(self):
|
||||
"""手动加载YOLOv5模型(不依赖torch.hub)"""
|
||||
try:
|
||||
# 检查YOLOv5目录是否存在,不存在则克隆
|
||||
yolov5_dir = os.path.join(BASE_DIR, "tools", "yolov5")
|
||||
if not os.path.exists(yolov5_dir):
|
||||
logger.info("YOLOv5目录不存在,正在克隆仓库...")
|
||||
os.makedirs(os.path.dirname(yolov5_dir), exist_ok=True)
|
||||
|
||||
# 克隆YOLOv5仓库
|
||||
subprocess.run(
|
||||
["git", "clone", "https://github.com/ultralytics/yolov5.git", yolov5_dir],
|
||||
check=True
|
||||
)
|
||||
|
||||
# 添加YOLOv5目录到系统路径
|
||||
sys.path.insert(0, yolov5_dir)
|
||||
|
||||
try:
|
||||
# 尝试导入YOLOv5模块
|
||||
from models.common import DetectMultiBackend
|
||||
from utils.torch_utils import select_device
|
||||
from utils.general import check_img_size, non_max_suppression, scale_coords
|
||||
|
||||
# 加载模型
|
||||
self.model = DetectMultiBackend(MODEL.get("weights"), device=self.device)
|
||||
self.stride = self.model.stride
|
||||
self.names = self.model.names
|
||||
|
||||
# 设置参数
|
||||
self.imgsz = check_img_size((640, 640), s=self.stride)
|
||||
|
||||
# 使用半精度
|
||||
if self.use_half_precision:
|
||||
self.model.half()
|
||||
|
||||
# 保存必要的函数
|
||||
self.non_max_suppression = non_max_suppression
|
||||
self.scale_coords = scale_coords
|
||||
|
||||
# 预热模型
|
||||
dummy_img = torch.zeros((1, 3, self.imgsz[0], self.imgsz[1]), device=self.device)
|
||||
if self.use_half_precision:
|
||||
dummy_img = dummy_img.half()
|
||||
|
||||
with torch.no_grad():
|
||||
for _ in range(2):
|
||||
self.model(dummy_img)
|
||||
|
||||
logger.info("手动加载YOLOv5模型成功")
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"导入YOLOv5模块失败: {e}")
|
||||
# 尝试使用更简单的模型
|
||||
self._load_fallback_model()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"手动加载YOLOv5模型失败: {e}")
|
||||
# 尝试使用更简单的模型
|
||||
self._load_fallback_model()
|
||||
|
||||
def _load_fallback_model(self):
|
||||
"""加载备用模型(使用PyTorch内置模型)"""
|
||||
logger.info("尝试加载备用模型 (PyTorch YOLO)")
|
||||
|
||||
try:
|
||||
# 使用PyTorch的预训练模型
|
||||
from torchvision.models.detection import fasterrcnn_resnet50_fpn
|
||||
|
||||
self.model = fasterrcnn_resnet50_fpn(pretrained=True)
|
||||
self.model.to(self.device)
|
||||
self.model.eval()
|
||||
|
||||
# 备用模型的类别
|
||||
self.names = [
|
||||
'background', 'monster', 'boss', 'door', 'item', 'npc', 'player',
|
||||
'hp_bar', 'mp_bar', 'skill_ready', 'cooldown'
|
||||
]
|
||||
|
||||
# 标记使用备用模型
|
||||
self.using_fallback = True
|
||||
|
||||
logger.info("备用模型加载成功")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载备用模型失败: {e}")
|
||||
raise
|
||||
|
||||
def _load_onnx_model(self):
|
||||
"""加载ONNX版YOLO模型"""
|
||||
def _initialize_onnx_model(self, weights_path):
|
||||
"""初始化ONNX YOLO模型"""
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
|
||||
# 设置ONNX运行时参数
|
||||
if self.device == "cuda":
|
||||
# 设置ONNX运行时选项
|
||||
sess_options = ort.SessionOptions()
|
||||
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||
|
||||
# 选择运行设备
|
||||
if self.device.startswith("cuda") and "CUDAExecutionProvider" in ort.get_available_providers():
|
||||
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
|
||||
else:
|
||||
providers = ['CPUExecutionProvider']
|
||||
|
||||
# 检查ONNX模型文件
|
||||
onnx_path = MODEL.get("weights", "")
|
||||
if not onnx_path.endswith('.onnx'):
|
||||
onnx_path = onnx_path + '.onnx'
|
||||
|
||||
if not os.path.exists(onnx_path):
|
||||
logger.warning(f"找不到ONNX模型: {onnx_path}")
|
||||
logger.info("尝试导出PyTorch模型到ONNX格式...")
|
||||
|
||||
# 尝试加载PyTorch模型并导出为ONNX
|
||||
self._load_yolov5_custom()
|
||||
self._export_to_onnx(onnx_path)
|
||||
self.device = "cpu"
|
||||
|
||||
# 创建ONNX会话
|
||||
self.onnx_session = ort.InferenceSession(onnx_path, providers=providers)
|
||||
self.model = ort.InferenceSession(weights_path, sess_options=sess_options,
|
||||
providers=providers)
|
||||
|
||||
# 获取模型输入输出名称
|
||||
self.input_name = self.onnx_session.get_inputs()[0].name
|
||||
self.output_names = [output.name for output in self.onnx_session.get_outputs()]
|
||||
# 获取模型输入输出信息
|
||||
self.input_names = [input.name for input in self.model.get_inputs()]
|
||||
self.output_names = [output.name for output in self.model.get_outputs()]
|
||||
|
||||
# 加载类别名称
|
||||
if os.path.exists(MODEL.get("class_names", "")):
|
||||
with open(MODEL.get("class_names"), "r") as f:
|
||||
self.class_names = yaml.safe_load(f)
|
||||
else:
|
||||
self.class_names = ["object"]
|
||||
|
||||
# 标记使用ONNX
|
||||
self.using_onnx = True
|
||||
self.model_type = "onnx"
|
||||
logger.info(f"已加载ONNX模型,提供者: {self.model.get_providers()}")
|
||||
|
||||
# 预热模型
|
||||
self._warmup_onnx_model()
|
||||
dummy_input = np.random.rand(1, 3, self.img_size, self.img_size).astype(np.float32)
|
||||
input_feed = {self.input_names[0]: dummy_input}
|
||||
self.model.run(self.output_names, input_feed)
|
||||
|
||||
except ImportError:
|
||||
logger.error("缺少ONNX运行时依赖,请安装onnxruntime或onnxruntime-gpu")
|
||||
logger.info("尝试使用PyTorch模型代替...")
|
||||
self._load_yolov5_custom()
|
||||
logger.error("无法导入onnxruntime,请安装: pip install onnxruntime-gpu 或 onnxruntime")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"加载ONNX模型失败: {e}")
|
||||
logger.info("尝试使用PyTorch模型代替...")
|
||||
self._load_yolov5_custom()
|
||||
|
||||
def _export_to_onnx(self, onnx_path):
|
||||
"""将PyTorch模型导出为ONNX格式"""
|
||||
try:
|
||||
import torch.onnx
|
||||
|
||||
# 准备导出
|
||||
dummy_input = torch.randn(1, 3, 640, 640, device=self.device)
|
||||
if self.use_half_precision:
|
||||
dummy_input = dummy_input.half()
|
||||
|
||||
# 导出ONNX
|
||||
torch.onnx.export(
|
||||
self.model,
|
||||
dummy_input,
|
||||
onnx_path,
|
||||
verbose=False,
|
||||
opset_version=12,
|
||||
input_names=['images'],
|
||||
output_names=['output']
|
||||
)
|
||||
|
||||
logger.info(f"PyTorch模型成功导出为ONNX格式: {onnx_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"导出ONNX模型失败: {e}")
|
||||
logger.error(f"初始化ONNX模型失败: {e}")
|
||||
raise
|
||||
|
||||
def _warmup_model(self):
|
||||
"""预热模型(运行空白图像以初始化)"""
|
||||
dummy_img = torch.zeros((1, 3, 640, 640), device=self.device)
|
||||
if self.use_half_precision:
|
||||
dummy_img = dummy_img.half()
|
||||
|
||||
# 进行多次推理预热
|
||||
with torch.no_grad():
|
||||
for _ in range(2):
|
||||
self.model(dummy_img)
|
||||
|
||||
logger.info("模型预热完成")
|
||||
|
||||
def _warmup_onnx_model(self):
|
||||
"""预热ONNX模型"""
|
||||
dummy_img = np.zeros((1, 3, 640, 640), dtype=np.float32)
|
||||
|
||||
# 进行多次推理预热
|
||||
for _ in range(2):
|
||||
self.onnx_session.run(self.output_names, {self.input_name: dummy_img})
|
||||
|
||||
logger.info("ONNX模型预热完成")
|
||||
"""预热模型,避免首次推理延迟"""
|
||||
try:
|
||||
logger.info("预热模型...")
|
||||
|
||||
if self.model_type == "yolov8":
|
||||
# YOLOv8预热
|
||||
dummy_image = np.random.randint(0, 255, (self.img_size, self.img_size, 3), dtype=np.uint8)
|
||||
self.model(dummy_image, verbose=False)
|
||||
|
||||
elif self.model_type == "yolov5":
|
||||
# YOLOv5预热
|
||||
dummy_image = torch.zeros((1, 3, self.img_size, self.img_size), device=self.device)
|
||||
if self.half_precision and self.device != "cpu":
|
||||
dummy_image = dummy_image.half()
|
||||
|
||||
self.model(dummy_image)
|
||||
|
||||
logger.info("模型预热完成")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"模型预热出错: {e}")
|
||||
|
||||
def detect(self, image):
|
||||
"""
|
||||
对图像进行目标检测
|
||||
执行目标检测
|
||||
|
||||
参数:
|
||||
image (PIL.Image): 输入图像
|
||||
|
||||
返回:
|
||||
list: 检测结果,每个结果包含边界框、类别和置信度
|
||||
list: 检测结果列表
|
||||
"""
|
||||
try:
|
||||
start_time = time.time()
|
||||
|
||||
# 根据模型类型选择检测方法
|
||||
if hasattr(self, 'using_onnx') and self.using_onnx:
|
||||
# 检查模型是否已初始化
|
||||
if self.model is None:
|
||||
logger.error("模型未初始化")
|
||||
return []
|
||||
|
||||
# 选择相应的检测方法
|
||||
if self.model_type == "yolov8":
|
||||
detections = self._detect_yolov8(image)
|
||||
elif self.model_type == "yolov5":
|
||||
detections = self._detect_yolov5(image)
|
||||
elif self.model_type == "onnx":
|
||||
detections = self._detect_onnx(image)
|
||||
elif hasattr(self, 'using_fallback') and self.using_fallback:
|
||||
detections = self._detect_fallback(image)
|
||||
else:
|
||||
detections = self._detect_pytorch(image)
|
||||
logger.error(f"不支持的模型类型: {self.model_type}")
|
||||
return []
|
||||
|
||||
# 记录推理时间
|
||||
inference_time = time.time() - start_time
|
||||
self.inference_times.append(inference_time)
|
||||
|
||||
# 计算平均推理时间
|
||||
avg_time = sum(self.inference_times) / len(self.inference_times)
|
||||
|
||||
if len(self.inference_times) % 10 == 0:
|
||||
logger.debug(f"平均推理时间: {avg_time:.3f}秒, 当前: {inference_time:.3f}秒")
|
||||
|
||||
return detections
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"目标检测出错: {e}")
|
||||
return []
|
||||
|
||||
def _detect_pytorch(self, image):
|
||||
"""使用PyTorch模型进行检测"""
|
||||
# 在GPU上进行推理
|
||||
with torch.no_grad():
|
||||
# 处理不同的模型接口
|
||||
if hasattr(self, 'stride') and hasattr(self, 'names'):
|
||||
# 自定义加载的YOLOv5
|
||||
# 转换图像
|
||||
img = self._prepare_image_custom(image)
|
||||
|
||||
# 推理
|
||||
output = self.model(img)
|
||||
|
||||
# 处理输出
|
||||
pred = self.non_max_suppression(output[0], self.conf_threshold, self.iou_threshold)
|
||||
|
||||
# 解析结果
|
||||
detections = []
|
||||
for det in pred[0]:
|
||||
x1, y1, x2, y2, conf, cls = det
|
||||
detections.append({
|
||||
'bbox': [float(x1), float(y1), float(x2), float(y2)],
|
||||
'confidence': float(conf),
|
||||
'class_id': int(cls),
|
||||
'class_name': self.names[int(cls)]
|
||||
})
|
||||
def _detect_yolov5(self, image):
|
||||
"""使用YOLOv5模型进行检测"""
|
||||
# 转换PIL图像为所需格式
|
||||
if isinstance(image, Image.Image):
|
||||
# PIL图像直接传给模型
|
||||
results = self.model(image, size=self.img_size)
|
||||
else:
|
||||
# 转换numpy数组为PIL图像
|
||||
image = Image.fromarray(np.array(image))
|
||||
results = self.model(image, size=self.img_size)
|
||||
|
||||
# 提取结果
|
||||
predictions = results.xyxy[0].cpu().numpy() # xyxy格式,(n, 6) - x1, y1, x2, y2, conf, cls
|
||||
|
||||
# 转换为标准格式
|
||||
detections = []
|
||||
for pred in predictions:
|
||||
x1, y1, x2, y2, conf, cls_id = pred
|
||||
cls_id = int(cls_id)
|
||||
|
||||
# 确保类别ID在范围内
|
||||
if cls_id < len(self.class_names):
|
||||
class_name = self.class_names[cls_id]
|
||||
else:
|
||||
# 标准torch.hub加载的模型
|
||||
results = self.model(image)
|
||||
|
||||
# 处理结果
|
||||
detections = []
|
||||
for pred in results.xyxy[0].cpu().numpy():
|
||||
x1, y1, x2, y2, conf, cls = pred
|
||||
detections.append({
|
||||
'bbox': [float(x1), float(y1), float(x2), float(y2)],
|
||||
'confidence': float(conf),
|
||||
'class_id': int(cls),
|
||||
'class_name': self.model.names[int(cls)]
|
||||
})
|
||||
class_name = f"class_{cls_id}"
|
||||
|
||||
detections.append({
|
||||
"bbox": [float(x1), float(y1), float(x2), float(y2)],
|
||||
"confidence": float(conf),
|
||||
"class_id": cls_id,
|
||||
"class_name": class_name
|
||||
})
|
||||
|
||||
return detections
|
||||
|
||||
def _detect_fallback(self, image):
|
||||
"""使用备用模型进行检测"""
|
||||
# 预处理图像
|
||||
img = self._prepare_image_pytorch(image)
|
||||
def _detect_yolov8(self, image):
|
||||
"""使用YOLOv8模型进行检测"""
|
||||
# 转换PIL图像为所需格式
|
||||
if isinstance(image, Image.Image):
|
||||
# 转换为numpy数组
|
||||
image_np = np.array(image)
|
||||
else:
|
||||
image_np = np.array(image)
|
||||
|
||||
# 进行推理
|
||||
with torch.no_grad():
|
||||
predictions = self.model(img)
|
||||
# 执行推理
|
||||
results = self.model(
|
||||
image_np,
|
||||
conf=self.conf_threshold,
|
||||
iou=self.iou_threshold,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# 处理结果
|
||||
# 提取结果
|
||||
detections = []
|
||||
for i, prediction in enumerate(predictions[0]['boxes']):
|
||||
score = predictions[0]['scores'][i].item()
|
||||
if score > self.conf_threshold:
|
||||
x1, y1, x2, y2 = prediction.tolist()
|
||||
class_id = predictions[0]['labels'][i].item()
|
||||
|
||||
for result in results:
|
||||
# 获取边界框
|
||||
boxes = result.boxes
|
||||
|
||||
for i in range(len(boxes)):
|
||||
# 提取坐标,conf和类别
|
||||
box = boxes[i]
|
||||
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
|
||||
conf = float(box.conf)
|
||||
cls_id = int(box.cls)
|
||||
|
||||
# 映射torchvision的COCO类别到我们的类别
|
||||
class_name = self.names[min(class_id, len(self.names) - 1)]
|
||||
# 确保类别ID在范围内
|
||||
if cls_id < len(self.class_names):
|
||||
class_name = self.class_names[cls_id]
|
||||
else:
|
||||
class_name = f"class_{cls_id}"
|
||||
|
||||
detections.append({
|
||||
'bbox': [float(x1), float(y1), float(x2), float(y2)],
|
||||
'confidence': float(score),
|
||||
'class_id': class_id,
|
||||
'class_name': class_name
|
||||
"bbox": [float(x1), float(y1), float(x2), float(y2)],
|
||||
"confidence": conf,
|
||||
"class_id": cls_id,
|
||||
"class_name": class_name
|
||||
})
|
||||
|
||||
return detections
|
||||
|
||||
def _detect_onnx(self, image):
|
||||
"""使用ONNX模型进行检测"""
|
||||
# 预处理图像
|
||||
input_tensor = self._prepare_image_onnx(image)
|
||||
# 准备输入图像
|
||||
if isinstance(image, Image.Image):
|
||||
# 转换PIL图像为numpy数组
|
||||
img = np.array(image.resize((self.img_size, self.img_size)))
|
||||
else:
|
||||
# 转换任何其他格式为numpy数组
|
||||
img = cv2.resize(np.array(image), (self.img_size, self.img_size))
|
||||
|
||||
# 运行推理
|
||||
outputs = self.onnx_session.run(self.output_names, {self.input_name: input_tensor})
|
||||
# 图像预处理
|
||||
img = img.transpose(2, 0, 1) # HWC -> CHW
|
||||
img = np.expand_dims(img, axis=0) # 添加batch维度
|
||||
img = img / 255.0 # 归一化到[0,1]
|
||||
img = img.astype(np.float32) # 转换为float32
|
||||
|
||||
# 解析输出(根据ONNX模型的输出格式可能需要调整)
|
||||
# 假设输出格式为 [batch_id, x1, y1, x2, y2, conf, class_id]
|
||||
predictions = outputs[0]
|
||||
# 执行推理
|
||||
input_feed = {self.input_names[0]: img}
|
||||
outputs = self.model.run(self.output_names, input_feed)
|
||||
|
||||
# 过滤低置信度预测
|
||||
mask = predictions[:, 5] > self.conf_threshold
|
||||
filtered_preds = predictions[mask]
|
||||
# 后处理
|
||||
# 注意:实际后处理可能因模型而异
|
||||
# 这里假设输出为[batch, num_detections, 6]格式,其中6为[x1, y1, x2, y2, conf, cls]
|
||||
predictions = outputs[0] # 假设第一个输出是检测结果
|
||||
|
||||
# 组织检测结果
|
||||
# 转换为标准格式
|
||||
detections = []
|
||||
for pred in filtered_preds:
|
||||
x1, y1, x2, y2, conf, cls = pred[1:7]
|
||||
cls_id = int(cls)
|
||||
|
||||
detections.append({
|
||||
'bbox': [float(x1), float(y1), float(x2), float(y2)],
|
||||
'confidence': float(conf),
|
||||
'class_id': cls_id,
|
||||
'class_name': self.class_names[cls_id] if cls_id < len(self.class_names) else "unknown"
|
||||
})
|
||||
|
||||
# 提取有效检测
|
||||
valid_preds = predictions[0] # 取第一个batch
|
||||
|
||||
for pred in valid_preds:
|
||||
if len(pred) >= 6: # 检查是否有6个元素
|
||||
x1, y1, x2, y2, conf, cls_id = pred[:6]
|
||||
|
||||
# 过滤低置信度检测
|
||||
if conf < self.conf_threshold:
|
||||
continue
|
||||
|
||||
cls_id = int(cls_id)
|
||||
|
||||
# 确保类别ID在范围内
|
||||
if cls_id < len(self.class_names):
|
||||
class_name = self.class_names[cls_id]
|
||||
else:
|
||||
class_name = f"class_{cls_id}"
|
||||
|
||||
# 将坐标从[0,1]缩放到原始图像尺寸
|
||||
if max(x1, y1, x2, y2) <= 1.0:
|
||||
# 坐标是归一化的,需要缩放到原始尺寸
|
||||
if hasattr(image, 'size'):
|
||||
orig_w, orig_h = image.size
|
||||
else:
|
||||
orig_h, orig_w = image.shape[:2]
|
||||
|
||||
x1 *= orig_w
|
||||
x2 *= orig_w
|
||||
y1 *= orig_h
|
||||
y2 *= orig_h
|
||||
|
||||
detections.append({
|
||||
"bbox": [float(x1), float(y1), float(x2), float(y2)],
|
||||
"confidence": float(conf),
|
||||
"class_id": cls_id,
|
||||
"class_name": class_name
|
||||
})
|
||||
|
||||
return detections
|
||||
|
||||
def _prepare_image_custom(self, image):
|
||||
"""为自定义加载的YOLOv5模型准备图像"""
|
||||
# 转换PIL图像为numpy数组
|
||||
img = np.array(image)
|
||||
|
||||
# 调整大小
|
||||
img = self._letterbox(img, new_shape=self.imgsz)[0]
|
||||
|
||||
# BGR转RGB
|
||||
img = img[:, :, ::-1].transpose(2, 0, 1)
|
||||
img = np.ascontiguousarray(img)
|
||||
|
||||
# 转换为PyTorch张量
|
||||
img = torch.from_numpy(img).to(self.device)
|
||||
img = img.half() if self.use_half_precision else img.float()
|
||||
img /= 255.0
|
||||
|
||||
# 增加批次维度
|
||||
if img.ndimension() == 3:
|
||||
img = img.unsqueeze(0)
|
||||
|
||||
return img
|
||||
|
||||
def _prepare_image_pytorch(self, image):
|
||||
"""为PyTorch模型准备图像"""
|
||||
# 转换PIL图像为PyTorch张量
|
||||
from torchvision import transforms
|
||||
|
||||
transform = transforms.Compose([
|
||||
transforms.ToTensor(),
|
||||
])
|
||||
|
||||
img = transform(image).unsqueeze(0).to(self.device)
|
||||
|
||||
return img
|
||||
|
||||
def _prepare_image_onnx(self, image):
|
||||
"""为ONNX模型准备图像"""
|
||||
# 调整图像大小
|
||||
img_size = MODEL.get("img_size", 640)
|
||||
image = image.resize((img_size, img_size), Image.LANCZOS)
|
||||
|
||||
# 转换为numpy数组
|
||||
img = np.array(image).astype(np.float32) / 255.0
|
||||
|
||||
# 从HWC转换为CHW格式
|
||||
img = img.transpose(2, 0, 1)
|
||||
|
||||
# 添加批次维度
|
||||
img = np.expand_dims(img, axis=0)
|
||||
|
||||
return img
|
||||
|
||||
def _letterbox(self, img, new_shape=(640, 640), color=(114, 114, 114)):
|
||||
"""调整图像大小并填充(YOLOv5风格)"""
|
||||
shape = img.shape[:2] # 当前形状 [高, 宽]
|
||||
|
||||
# 缩放比例 (新 / 旧)
|
||||
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
|
||||
|
||||
# 计算填充
|
||||
new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
|
||||
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
|
||||
|
||||
# 平均分配填充
|
||||
dw /= 2
|
||||
dh /= 2
|
||||
|
||||
# 调整大小
|
||||
if shape[::-1] != new_unpad:
|
||||
import cv2
|
||||
img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
# 添加边框
|
||||
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
|
||||
left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
|
||||
img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
|
||||
|
||||
return img, r, (dw, dh)
|
||||
|
||||
def get_class_names(self):
|
||||
"""获取类别名称列表"""
|
||||
if hasattr(self, 'using_onnx') and self.using_onnx:
|
||||
return self.class_names
|
||||
elif hasattr(self, 'using_fallback') and self.using_fallback:
|
||||
return self.names
|
||||
elif hasattr(self, 'names'):
|
||||
return self.names
|
||||
else:
|
||||
return self.model.names
|
||||
|
||||
def get_performance_stats(self):
|
||||
"""获取性能统计信息"""
|
||||
if not self.inference_times:
|
||||
return {"average_time": 0, "min_time": 0, "max_time": 0, "count": 0, "fps": 0}
|
||||
return {
|
||||
"average_time": 0,
|
||||
"min_time": 0,
|
||||
"max_time": 0,
|
||||
"fps": 0
|
||||
}
|
||||
|
||||
avg_time = sum(self.inference_times) / len(self.inference_times)
|
||||
|
||||
# 修复fps计算逻辑
|
||||
if avg_time > 0:
|
||||
fps = 1.0 / avg_time
|
||||
else:
|
||||
fps = 0
|
||||
|
||||
return {
|
||||
"average_time": avg_time,
|
||||
"min_time": min(self.inference_times),
|
||||
"max_time": max(self.inference_times),
|
||||
"count": len(self.inference_times),
|
||||
"fps": fps
|
||||
}
|
||||
"median_time": statistics.median(self.inference_times),
|
||||
"fps": 1.0 / avg_time if avg_time > 0 else 0
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 简单测试
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
try:
|
||||
# 初始化模型
|
||||
model = YOLOModel()
|
||||
|
||||
# 加载测试图像
|
||||
test_image_path = "data/training/images/test/sample_1.jpg"
|
||||
if os.path.exists(test_image_path):
|
||||
image = Image.open(test_image_path)
|
||||
|
||||
# 检测
|
||||
detections = model.detect(image)
|
||||
|
||||
# 打印结果
|
||||
print(f"检测到 {len(detections)} 个目标:")
|
||||
for det in detections:
|
||||
print(f"类别: {det['class_name']}, 置信度: {det['confidence']:.2f}, 边界框: {det['bbox']}")
|
||||
|
||||
# 打印性能
|
||||
perf = model.get_performance_stats()
|
||||
print(f"模型性能: {perf['average_time']*1000:.2f}ms, {perf['fps']:.2f} FPS")
|
||||
else:
|
||||
print(f"测试图像不存在: {test_image_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"测试失败: {e}")
|
||||