Compare commits

...

34 Commits

Author SHA1 Message Date
ray
6337567ddf Merge remote-tracking branch 'origin/ui' into ui 2025-11-04 14:47:38 +08:00
ray
d0e74f49d0 测试文件提交 2025-11-04 14:47:28 +08:00
Administrator
33b88145bd Merge remote-tracking branch 'origin/ui' into ui 2025-11-04 14:39:52 +08:00
ray
e6cd3658d0 测试文件提交 2025-11-04 14:39:33 +08:00
Administrator
a9e5181bce Merge remote-tracking branch 'origin/ui' into ui 2025-11-04 14:33:08 +08:00
Administrator
84c77ae459 1 2025-11-04 14:32:48 +08:00
ray
c5415156ed 测试文件提交 2025-11-04 11:39:07 +08:00
ray
b10333a308 测试文件提交 2025-11-04 11:32:16 +08:00
Administrator
c9b51f225b 1 2025-11-02 21:12:37 +08:00
Ray
73d1f87ec7 Merge remote-tracking branch 'main/ui' into ui
# Conflicts:
#	preview.py
2025-11-01 23:36:41 +08:00
Ray
4a0549114f 小bug修改 2025-11-01 16:32:07 +08:00
ray
eb896fc31e 多台控制bug调整 2025-10-31 00:24:30 +08:00
ray
fc87a3cf84 Merge remote-tracking branch 'origin/ui' into ui
# Conflicts:
#	preview.py
2025-10-30 23:41:57 +08:00
ray
7623e22e5c 多台控制 2025-10-30 23:39:27 +08:00
Ray
4ef7c0216d Merge remote-tracking branch 'main/ui' into ui 2025-10-29 18:15:27 +08:00
Ray
2399f87d57 图片前的版本 2025-10-29 18:15:12 +08:00
Ray
b87a26d386 采集卡bug修复 2025-10-29 18:11:25 +08:00
Ray
94fa69043b 采集卡bug修复 2025-10-29 17:55:54 +08:00
Ray
754501b933 采集卡bug修复 2025-10-29 17:50:33 +08:00
Ray
ed636f68f6 采集卡bug修复 2025-10-29 17:42:27 +08:00
Ray
069613fe09 采集卡bug修复 2025-10-29 17:24:44 +08:00
Ray
ac686997d1 采集卡bug修复 2025-10-29 17:10:32 +08:00
Ray
c6ccf2052f 采集卡bug修复 2025-10-29 17:05:47 +08:00
Ray
735eb79e0d 采集卡bug修复 2025-10-29 17:05:38 +08:00
Ray
b0ff800826 采集卡bug修复 2025-10-29 16:53:34 +08:00
Ray
d3d1299323 采集卡bug修复 2025-10-29 16:47:30 +08:00
Ray
7af3e2353a 采集卡bug修复 2025-10-29 16:28:06 +08:00
Ray
f0fc28d827 采集卡bug修复 2025-10-29 15:37:48 +08:00
Ray
3a8873acc2 采集卡bug修复 2025-10-29 15:34:28 +08:00
Ray
bcc971d528 采集卡bug修复 2025-10-29 15:27:50 +08:00
Ray
f7dbf223cb 采集卡bug修复 2025-10-29 15:21:44 +08:00
Ray
cd43334957 采集卡bug修复 2025-10-29 14:36:45 +08:00
Ray
0e16ec99c3 采集卡bug修复 2025-10-29 14:06:05 +08:00
Ray
3f1dd4e8c1 ui界面版本 2025-10-29 10:49:38 +08:00
24 changed files with 4698 additions and 157 deletions

2
.idea/huojv.iml generated
View File

@@ -2,7 +2,7 @@
<module type="PYTHON_MODULE" version="4"> <module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="dnf" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.9" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

2
.idea/misc.xml generated
View File

@@ -3,5 +3,5 @@
<component name="Black"> <component name="Black">
<option name="sdkName" value="D:\CONDA\anaconda3" /> <option name="sdkName" value="D:\CONDA\anaconda3" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="dnf" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
</project> </project>

99
CHANGELOG.md Normal file
View File

@@ -0,0 +1,99 @@
# 更新日志
## 最新更新 - 多组配置系统
### 主要变更
#### 1. 新增文件
- **config.py** - 配置管理器,支持多组配置
- **gui_config.py** - 可视化配置界面tkinter
- **preview.py** - 采集卡预览窗口系统
- **launcher.py** - 统一启动器
- **README.md** - 使用文档
- **config.json** - 配置文件(自动生成)
#### 2. 修改的文件
- **main.py** - 完全重写,支持配置系统
- 从配置文件加载参数
- 支持动态读取v值
- 移除硬编码配置
- **utils/mouse.py** - 移除硬编码串口
- 新增 `init_mouse_keyboard()` 函数
- 支持从配置初始化
- **utils/get_image.py** - 支持从配置初始化
- GetImage改为延迟初始化
### 核心功能
1. **多组配置管理**
- 支持创建多个配置组
- 每个配置组独立的串口、采集卡、速度设置
- 可视化切换活动配置
2. **可视化配置界面**
- GUI界面管理所有配置
- 实时编辑和保存
- 支持添加/删除配置组
3. **采集卡预览系统**
- 网格方式预览多个采集卡
- 点击放大查看
- 实时画面更新
4. **灵活的参数配置**
- 串口配置(端口、波特率)
- 采集卡配置(索引、分辨率)
- 移动速度(v值)配置
- 预览窗口配置
### 使用方式
#### 旧版本(已保留 main_old.py
```bash
python main_old.py # 直接运行,使用硬编码配置
```
#### 新版本
```bash
# 方式1: 使用启动器(推荐)
python launcher.py
# 方式2: 直接运行
python main.py # 读取config.json的active配置
```
### 配置示例
```json
{
"groups": [
{
"name": "配置组1",
"serial_port": "COM6",
"serial_baudrate": 9600,
"camera_index": 0,
"camera_width": 1920,
"camera_height": 1080,
"move_velocity": 470, // ← move_to函数中的v值
"active": true
}
]
}
```
### 兼容性
- ✅ 保留所有原有功能
- ✅ YOLO模型无需修改
- ✅ 游戏逻辑完全兼容
- ⚠️ 旧代码会使用默认配置
### 下一步
如需添加更多配置:
1.`config.py``default_config` 中添加新字段
2.`gui_config.py` 中添加对应的输入框
3.`main.py` 中读取并使用新配置

161
PREVIEW_CONFIG_GUIDE.md Normal file
View File

@@ -0,0 +1,161 @@
# 预览配置说明
## 📖 预览配置参数说明
预览配置位于配置界面的**"预览配置"**区域,用于控制采集卡预览窗口的显示方式。
### 配置参数
#### 1. **预览宽度** (`preview_width`)
- **说明**:预览窗口的宽度(像素)
- **默认值**1000
- **建议值**
- 单个采集卡800-1200
- 2个采集卡1000-1600
- 4个采集卡1200-2000
- **用途**:控制预览窗口的整体宽度
#### 2. **预览高度** (`preview_height`)
- **说明**:预览窗口的高度(像素)
- **默认值**700
- **建议值**
- 单个采集卡600-900
- 2个采集卡700-1000
- 4个采集卡800-1200
- **用途**:控制预览窗口的整体高度
#### 3. **预览列数** (`preview_columns`)
- **说明**:预览窗口中每行显示的采集卡数量
- **默认值**2
- **范围**1-4推荐
- **用途**:决定采集卡预览的网格布局列数
- **示例**
- 2列适合2-4个采集卡
- 1列适合单个采集卡全屏显示
- 3列适合3个或6个采集卡
#### 4. **预览行数** (`preview_rows`)
- **说明**:预览窗口中每列显示的采集卡数量
- **默认值**2
- **范围**1-4推荐
- **用途**:决定采集卡预览的网格布局行数
- **示例**
- 2行配合2列可以显示4个采集卡
- 1行适合1-3个采集卡水平排列
- 3行适合3个或更多采集卡
### 📐 配置示例
#### 示例1单个采集卡全屏显示
```
预览宽度1200
预览高度900
预览列数1
预览行数1
```
**效果**:单个采集卡占据整个预览窗口
#### 示例22个采集卡并排显示
```
预览宽度1600
预览高度800
预览列数2
预览行数1
```
**效果**:两个采集卡水平并排显示
#### 示例34个采集卡网格显示2x2
```
预览宽度1600
预览高度1200
预览列数2
预览行数2
```
**效果**四个采集卡以2x2网格方式显示
#### 示例46个采集卡网格显示3x2
```
预览宽度1800
预览高度1200
预览列数3
预览行数2
```
**效果**六个采集卡以3x2网格方式显示
### 🎯 配置技巧
1. **根据采集卡数量调整**
- 采集卡数量 = 列数 × 行数
- 例如3个采集卡可以用 3列×1行 或 2列×2行最后一个位置为空
2. **根据屏幕尺寸调整**
- 确保预览窗口能够完整显示在你的屏幕上
- 如果屏幕较小,可以减小预览宽度和高度
3. **性能考虑**
- 预览窗口越大CPU占用可能越高
- 推荐在1000-1600像素宽度范围内
4. **视觉体验**
- 保持预览窗口的宽高比接近采集卡的宽高比
- 例如采集卡是1920×1080预览窗口可以是1600×900
### 🔧 如何配置
1. **打开配置界面**
```bash
python gui_config.py
```
2. **找到预览配置区域**
- 在配置界面右侧找到"预览配置"区域
3. **修改参数**
- 在对应的输入框中输入你想要的数值
- 点击"保存配置"按钮
4. **测试预览**
- 点击"启动预览"按钮查看效果
- 如果效果不理想,可以返回继续调整
### ⚠️ 注意事项
1. **字段名必须正确**
- 配置使用 `preview_width`、`preview_height`、`preview_columns`、`preview_rows`
- 不要使用旧版本的 `preview_window_width` 和 `preview_window_height`
2. **数值类型**
- 所有数值必须是整数
- 宽度和高度建议在 500-3000 像素之间
- 列数和行数建议在 1-4 之间
3. **配置文件位置**
- 配置文件保存在 `config.json` 文件中的 `display` 部分
- 修改后会立即生效(需要重启预览窗口)
### 📝 配置文件示例
```json
{
"groups": [...],
"display": {
"preview_width": 1000,
"preview_height": 700,
"preview_columns": 2,
"preview_rows": 2,
"show_preview": true
}
}
```
### 🚀 快速开始
1. 默认配置2x2网格1000x700窗口适合大多数场景
2. 如果有多个采集卡先尝试2列×2行的布局
3. 如果只有一个采集卡设置为1列×1行增大窗口尺寸
4. 根据实际效果微调,直到满意为止
---
**提示**预览窗口可以点击任意采集卡画面来放大查看。放大后的窗口会在新窗口中显示大小固定为1280×720。

141
README.md Normal file
View File

@@ -0,0 +1,141 @@
# 火炬之光自动化脚本
## 功能特点
-**多配置组管理** - 支持多组配置,可同时管理多套采集卡和串口设置
- 🖥️ **可视化配置** - 提供图形化界面管理所有配置
- 👁️ **采集卡预览** - 多窗口网格预览,点击可放大查看
- ⚙️ **灵活配置** - 所有参数(串口、采集卡、移动速度等)均可配置
- 🤖 **智能自动化** - 基于YOLO目标检测的游戏自动化
## 快速开始
### 1. 安装依赖
```bash
pip install opencv-python ultralytics ddddocr ch9329Comm serial PIL numpy
```
### 2. 启动程序
```bash
python launcher.py
```
启动器提供三个选项:
- **⚙️ 配置管理** - 配置串口、采集卡等参数
- **👁️ 采集卡预览** - 实时预览采集卡画面
- **🚀 启动自动化** - 启动游戏自动化脚本
### 3. 配置设置
首次使用请先点击"配置管理"
1. **添加配置组** - 点击"添加组"创建新配置
2. **设置参数**
- 串口:选择设备串口号(如 COM6
- 波特率设置串口波特率默认9600
- 采集卡索引:选择采集卡设备号
- 采集宽度/高度:设置采集分辨率
- 移动速度设置角色移动速度v值
3. **设为活动** - 选择要使用的配置组并设为活动
4. **保存配置** - 点击"保存配置"
### 4. 预览采集卡
点击"采集卡预览"可以:
- 网格方式查看所有活动的采集卡
- 点击任意窗口可放大查看
- 实时显示采集卡画面
### 5. 启动自动化
配置完成后,点击"启动自动化"开始游戏自动化。
## 配置说明
### 配置组结构
```json
{
"groups": [
{
"name": "配置组1",
"serial_port": "COM6",
"serial_baudrate": 9600,
"camera_index": 0,
"camera_width": 1920,
"camera_height": 1080,
"move_velocity": 470,
"active": false
}
],
"display": {
"preview_window_width": 640,
"preview_window_height": 360,
"preview_columns": 2,
"preview_rows": 2,
"show_preview": true
}
}
```
### 关键参数说明
- **serial_port**: 串口号,如 COM1, COM6
- **serial_baudrate**: 波特率,通常为 9600
- **camera_index**: 采集卡索引0/1/2...
- **camera_width/height**: 采集分辨率
- **move_velocity**: 角色移动速度v值数值越大移动越快
## 文件结构
```
huojv/
├── launcher.py # 启动器
├── main.py # 主程序(自动游戏)
├── config.py # 配置管理器
├── gui_config.py # 配置GUI界面
├── preview.py # 预览窗口
├── utils/
│ ├── get_image.py # 采集卡管理
│ ├── mouse.py # 鼠标控制
│ ├── shizi.py # OCR识别
│ └── ...
├── best.pt # YOLO模型战斗
├── best0.pt # YOLO模型城镇
└── config.json # 配置文件(自动生成)
```
## 使用流程
1. 运行 `python launcher.py`
2. 点击"配置管理"设置参数
3. 点击"采集卡预览"测试采集
4. 点击"启动自动化"开始游戏
5. 享受自动化!
## 注意事项
- 确保采集卡和串口设备已正确连接
- 串口号和采集卡索引需要根据实际情况设置
- 移动速度(v值)需要根据游戏角色属性调整
- 配置文件保存在 `config.json`
## 问题排查
### 无法打开串口
- 检查串口号是否正确
- 确认串口设备已连接
- 检查其他程序是否占用串口
### 无法打开采集卡
- 检查采集卡索引是否正确0, 1, 2...
- 确认采集卡驱动已安装
- 尝试不同的采集卡索引
### 识别不准确
- 调整移动速度(v值)
- 检查采集卡画面是否清晰
- 确认游戏窗口位置正确

BIN
best0.pt

Binary file not shown.

33
config.json Normal file
View File

@@ -0,0 +1,33 @@
{
"groups": [
{
"name": "电脑2",
"serial_port": "COM13",
"serial_baudrate": 9600,
"camera_index": 1,
"camera_width": 1920,
"camera_height": 1080,
"move_velocity": 400,
"active": false
},
{
"name": "服务器",
"serial_port": "COM12",
"serial_baudrate": 9600,
"camera_index": 0,
"camera_width": 1920,
"camera_height": 1080,
"move_velocity": 470,
"active": true
}
],
"display": {
"preview_width": 1920,
"preview_height": 1080,
"preview_columns": 1,
"preview_rows": 1,
"show_preview": true,
"preview_multi_window": false,
"preview_use_all_groups": false
}
}

122
config.py Normal file
View File

@@ -0,0 +1,122 @@
import json
import os
class ConfigManager:
"""配置管理器"""
def __init__(self, config_file='config.json'):
self.config_file = config_file
self.default_config = {
"groups": [
{
"name": "配置组1",
"serial_port": "COM6",
"serial_baudrate": 9600,
"camera_index": 0,
"camera_width": 1920,
"camera_height": 1080,
"move_velocity": 470,
"active": False
}
],
"display": {
"preview_width": 1000,
"preview_height": 700,
"preview_columns": 2,
"preview_rows": 2,
"show_preview": True,
"preview_multi_window": False,
"preview_use_all_groups": True
}
}
self.config = self.load_config()
def load_config(self):
"""加载配置文件"""
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
# 确保有默认结构
if 'groups' not in config:
config['groups'] = self.default_config['groups']
if 'display' not in config:
config['display'] = self.default_config['display']
return config
except Exception as e:
print(f"加载配置文件失败: {e}")
return self.default_config.copy()
return self.default_config.copy()
def save_config(self):
"""保存配置文件"""
try:
import os
# 确保目录存在
config_dir = os.path.dirname(os.path.abspath(self.config_file))
if config_dir and not os.path.exists(config_dir):
os.makedirs(config_dir, exist_ok=True)
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(self.config, f, ensure_ascii=False, indent=4)
print(f"✅ 配置文件已保存到: {os.path.abspath(self.config_file)}")
return True
except Exception as e:
print(f"❌ 保存配置文件失败: {e}")
import traceback
traceback.print_exc()
return False
def add_group(self, name=None):
"""添加配置组"""
if name is None:
name = f"配置组{len(self.config['groups']) + 1}"
new_group = {
"name": name,
"serial_port": "COM6",
"serial_baudrate": 9600,
"camera_index": len(self.config['groups']),
"camera_width": 1920,
"camera_height": 1080,
"move_velocity": 470,
"active": False
}
self.config['groups'].append(new_group)
return new_group
def delete_group(self, index):
"""删除配置组"""
if 0 <= index < len(self.config['groups']):
del self.config['groups'][index]
return True
return False
def update_group(self, index, **kwargs):
"""更新配置组"""
if 0 <= index < len(self.config['groups']):
self.config['groups'][index].update(kwargs)
return True
return False
def get_active_group(self):
"""获取当前活动的配置组"""
for group in self.config['groups']:
if group.get('active', False):
return group
return None
def set_active_group(self, index):
"""设置活动的配置组"""
for i, group in enumerate(self.config['groups']):
group['active'] = (i == index)
return True
def get_group_by_index(self, index):
"""根据索引获取配置组"""
if 0 <= index < len(self.config['groups']):
return self.config['groups'][index]
return None
# 全局配置管理器实例
config_manager = ConfigManager()

560
gui_config.py Normal file
View File

@@ -0,0 +1,560 @@
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
import cv2
from config import config_manager
import threading
import serial.tools.list_ports
class ConfigGUI:
"""配置GUI界面"""
def __init__(self):
self.root = tk.Tk()
self.root.title("配置管理 - 火炬之光自动化")
self.root.geometry("1000x720")
self.root.minsize(900, 600) # 设置最小尺寸
self.selected_index = 0
self.preview_thread = None # 添加预览线程引用
self.setup_ui()
self.load_current_config()
def setup_ui(self):
"""设置UI界面"""
# 左侧:配置组列表
left_frame = ttk.Frame(self.root, padding="10")
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False)
ttk.Label(left_frame, text="配置组列表", font=('Arial', 12, 'bold')).pack()
# 配置组列表
self.group_listbox = tk.Listbox(left_frame, width=20, height=25)
self.group_listbox.pack(fill=tk.BOTH, expand=True, pady=5)
self.group_listbox.bind('<<ListboxSelect>>', self.on_group_select)
# 按钮
btn_frame = ttk.Frame(left_frame)
btn_frame.pack(fill=tk.X)
ttk.Button(btn_frame, text="添加组", command=self.add_group).pack(fill=tk.X, pady=2)
ttk.Button(btn_frame, text="删除组", command=self.delete_group).pack(fill=tk.X, pady=2)
ttk.Button(btn_frame, text="设为活动", command=self.set_active).pack(fill=tk.X, pady=2)
# 右侧:配置详情
right_frame = ttk.Frame(self.root, padding="10")
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
ttk.Label(right_frame, text="配置详情", font=('Arial', 12, 'bold')).pack()
# 配置项
self.config_vars = {}
# 基本配置
basic_frame = ttk.LabelFrame(right_frame, text="基本配置", padding="10")
basic_frame.pack(fill=tk.X, pady=5)
self.create_entry(basic_frame, "name", "配置组名称:")
# 采集卡索引:使用下拉+扫描
cam_row = ttk.Frame(basic_frame)
cam_row.pack(fill=tk.X, pady=3)
ttk.Label(cam_row, text="采集卡索引:", width=15).pack(side=tk.LEFT)
self.camera_index_var = tk.StringVar()
self.camera_index_cb = ttk.Combobox(cam_row, textvariable=self.camera_index_var, width=23, state="readonly")
self.camera_index_cb.pack(side=tk.LEFT, padx=5)
ttk.Button(cam_row, text="扫描采集卡", command=self.scan_cameras).pack(side=tk.LEFT, padx=5)
# 将变量放进config_vars统一管理
self.config_vars["camera_index"] = self.camera_index_var
self.create_entry(basic_frame, "camera_width", "采集宽度:")
self.create_entry(basic_frame, "camera_height", "采集高度:")
# 串口配置
serial_frame = ttk.LabelFrame(right_frame, text="串口配置", padding="10")
serial_frame.pack(fill=tk.X, pady=5)
# 串口:使用下拉+扫描
port_row = ttk.Frame(serial_frame)
port_row.pack(fill=tk.X, pady=3)
ttk.Label(port_row, text="串口:", width=15).pack(side=tk.LEFT)
self.serial_port_var = tk.StringVar()
self.serial_port_cb = ttk.Combobox(port_row, textvariable=self.serial_port_var, width=23, state="readonly")
self.serial_port_cb.pack(side=tk.LEFT, padx=5)
ttk.Button(port_row, text="扫描串口", command=self.scan_ports).pack(side=tk.LEFT, padx=5)
# 将变量放进config_vars统一管理
self.config_vars["serial_port"] = self.serial_port_var
self.create_entry(serial_frame, "serial_baudrate", "波特率:")
# 游戏配置
game_frame = ttk.LabelFrame(right_frame, text="游戏配置", padding="10")
game_frame.pack(fill=tk.X, pady=5)
self.create_entry(game_frame, "move_velocity", "移动速度(v值):")
# 预览配置
preview_frame = ttk.LabelFrame(right_frame, text="预览配置", padding="10")
preview_frame.pack(fill=tk.X, pady=5)
self.create_entry(preview_frame, "preview_width", "预览宽度:", prefix="display")
self.create_entry(preview_frame, "preview_height", "预览高度:", prefix="display")
self.create_entry(preview_frame, "preview_columns", "预览列数:", prefix="display")
self.create_entry(preview_frame, "preview_rows", "预览行数:", prefix="display")
# 多窗口预览开关
multi_row = ttk.Frame(preview_frame)
multi_row.pack(fill=tk.X, pady=3)
ttk.Label(multi_row, text="多窗口预览:", width=15).pack(side=tk.LEFT)
self.preview_multi_window_var = tk.BooleanVar(value=False)
multi_cb = ttk.Checkbutton(multi_row, variable=self.preview_multi_window_var)
multi_cb.pack(side=tk.LEFT, padx=5)
self.config_vars["display_preview_multi_window"] = self.preview_multi_window_var
# 预览全部配置组开关
all_row = ttk.Frame(preview_frame)
all_row.pack(fill=tk.X, pady=3)
ttk.Label(all_row, text="预览全部配置组:", width=15).pack(side=tk.LEFT)
self.preview_use_all_groups_var = tk.BooleanVar(value=True)
all_cb = ttk.Checkbutton(all_row, variable=self.preview_use_all_groups_var)
all_cb.pack(side=tk.LEFT, padx=5)
self.config_vars["display_preview_use_all_groups"] = self.preview_use_all_groups_var
# 保存按钮
save_frame = ttk.Frame(right_frame)
save_frame.pack(fill=tk.X, pady=10)
ttk.Button(save_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=5)
ttk.Button(save_frame, text="启动预览", command=self.start_preview).pack(side=tk.LEFT, padx=5)
# 启动程序按钮组
start_frame = ttk.Frame(right_frame)
start_frame.pack(fill=tk.X, pady=5)
ttk.Button(start_frame, text="启动单个配置组", command=self.start_program).pack(side=tk.LEFT, padx=5)
ttk.Button(start_frame, text="启动多个配置组", command=self.start_multi_program).pack(side=tk.LEFT, padx=5)
def create_entry(self, parent, key, label, prefix=None):
"""创建输入框"""
frame = ttk.Frame(parent)
frame.pack(fill=tk.X, pady=3)
ttk.Label(frame, text=label, width=15).pack(side=tk.LEFT)
var = tk.StringVar()
entry = ttk.Entry(frame, textvariable=var, width=25)
entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
# 保存变量引用
if prefix:
full_key = f"{prefix}_{key}"
else:
full_key = key
self.config_vars[full_key] = var
def load_current_config(self):
"""加载当前配置到界面"""
# 加载配置组列表
self.update_group_list()
# 如果有配置组,默认选择第一个
if len(config_manager.config['groups']) > 0:
self.selected_index = 0
self.group_listbox.selection_set(0)
self.load_group_config(0)
# 加载预览配置
display = config_manager.config.get('display', {})
for key in ['preview_width', 'preview_height', 'preview_columns', 'preview_rows']:
if key in display:
self.config_vars[f"display_{key}"].set(str(display[key]))
# 多窗口预览
if 'preview_multi_window' in display:
self.preview_multi_window_var.set(bool(display['preview_multi_window']))
# 预览全部配置组
if 'preview_use_all_groups' in display:
self.preview_use_all_groups_var.set(bool(display['preview_use_all_groups']))
# 初次自动扫描一次采集卡和串口
try:
self.scan_cameras()
except Exception:
pass
try:
self.scan_ports()
except Exception:
pass
def update_group_list(self):
"""更新配置组列表"""
self.group_listbox.delete(0, tk.END)
for i, group in enumerate(config_manager.config['groups']):
status = "" if group.get('active', False) else " "
self.group_listbox.insert(tk.END, f"{status} {group['name']}")
def on_group_select(self, event):
"""选择配置组"""
selection = self.group_listbox.curselection()
if selection:
self.selected_index = selection[0]
self.load_group_config(self.selected_index)
def load_group_config(self, index):
"""加载配置组详情"""
group = config_manager.get_group_by_index(index)
if group:
for key, var in self.config_vars.items():
if not key.startswith('display_'):
if key in group:
var.set(str(group[key]))
# 确保下拉框列表与当前值一致
if group and 'camera_index' in group:
# 如果当前索引不在选项里,追加
values = list(self.camera_index_cb.cget('values')) if self.camera_index_cb.cget('values') else []
display_value = f"{group['camera_index']}"
if display_value not in values:
values.append(display_value)
self.camera_index_cb['values'] = values
self.camera_index_cb.set(display_value)
# 确保串口下拉框列表与当前值一致
if group and 'serial_port' in group:
# 如果当前串口不在选项里,追加
values = list(self.serial_port_cb.cget('values')) if self.serial_port_cb.cget('values') else []
port_value = group['serial_port']
if port_value not in values:
values.append(port_value)
self.serial_port_cb['values'] = values
self.serial_port_cb.set(port_value)
def scan_cameras(self, max_index: int = 10):
"""扫描系统可用的采集卡索引,并填充下拉框"""
import warnings
import sys
import io
found = []
# 临时设置OpenCV日志级别兼容不同版本
import os
old_level = os.environ.get('OPENCV_LOG_LEVEL', '')
os.environ['OPENCV_LOG_LEVEL'] = 'SILENT' # 尝试更严格的级别
os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '0'
# 尝试设置日志级别不同版本的OpenCV API不同
try:
if hasattr(cv2, 'setLogLevel'):
# OpenCV 4.x
if hasattr(cv2, 'LOG_LEVEL_SILENT'):
cv2.setLogLevel(cv2.LOG_LEVEL_SILENT)
elif hasattr(cv2, 'LOG_LEVEL_ERROR'):
cv2.setLogLevel(cv2.LOG_LEVEL_ERROR)
elif hasattr(cv2, 'utils'):
cv2.utils.setLogLevel(0) # 0=SILENT/ERROR级别
except Exception:
pass
# 重定向stderr来捕获OpenCV的错误输出
old_stderr = sys.stderr
suppressed_output = io.StringIO()
try:
# 暂时重定向stderr以抑制OpenCV的错误消息
sys.stderr = suppressed_output
for idx in range(max_index + 1):
cap = None
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore')
# 尝试DSHOW后端
cap = cv2.VideoCapture(idx, cv2.CAP_DSHOW)
if not cap.isOpened():
# 回退默认后端再试
cap = cv2.VideoCapture(idx)
if cap.isOpened():
# 测试读取一帧,确保真正可用
ret, test_frame = cap.read()
if ret and test_frame is not None:
found.append(str(idx))
cap.release()
except Exception:
if cap:
try:
cap.release()
except:
pass
continue
finally:
# 恢复stderr
sys.stderr = old_stderr
# 恢复原来的日志级别
if old_level:
os.environ['OPENCV_LOG_LEVEL'] = old_level
else:
os.environ.pop('OPENCV_LOG_LEVEL', None)
os.environ.pop('OPENCV_IO_ENABLE_OPENEXR', None)
if not found:
found = ["0"] # 至少给一个默认项,避免为空
messagebox.showwarning("扫描完成", "未发现可用采集卡,已添加默认选项 0")
else:
messagebox.showinfo("扫描完成", f"发现可用采集卡索引: {', '.join(found)}")
self.camera_index_cb['values'] = found
# 若当前无选择,则选择第一项
if not self.camera_index_var.get() and found:
self.camera_index_cb.set(found[0])
def scan_ports(self):
"""扫描系统可用的串口,并填充下拉框"""
found_real = []
try:
ports = serial.tools.list_ports.comports()
found_real = [port.device for port in ports]
# 按端口名排序
found_real.sort(key=lambda x: int(x.replace('COM', '')) if x.replace('COM', '').isdigit() else 999)
except Exception as e:
print(f"扫描串口错误: {e}")
# 如果没有发现实际端口,使用默认端口列表
if not found_real:
found = ["COM1", "COM2", "COM3", "COM4", "COM5", "COM6"] # 默认给一些常见端口
messagebox.showwarning("警告", "未发现可用串口设备,已添加常用默认选项")
else:
found = found_real
messagebox.showinfo("扫描完成", f"发现可用串口: {', '.join(found)}")
self.serial_port_cb['values'] = found
# 若当前无选择,则选择第一项
if not self.serial_port_var.get() and found:
self.serial_port_cb.set(found[0])
def add_group(self):
"""添加配置组"""
name = simpledialog.askstring("添加配置组", "请输入配置组名称:", initialvalue=f"配置组{len(config_manager.config['groups'])+1}")
if name:
config_manager.add_group(name)
self.update_group_list()
messagebox.showinfo("成功", f"已添加配置组: {name}")
def delete_group(self):
"""删除配置组"""
selection = self.group_listbox.curselection()
if not selection:
messagebox.showwarning("警告", "请先选择要删除的配置组")
return
if messagebox.askyesno("确认", "确定要删除选中的配置组吗?"):
config_manager.delete_group(selection[0])
self.update_group_list()
messagebox.showinfo("成功", "配置组已删除")
def set_active(self):
"""设置为活动配置组"""
selection = self.group_listbox.curselection()
if not selection:
messagebox.showwarning("警告", "请先选择要设为活动的配置组")
return
config_manager.set_active_group(selection[0])
self.update_group_list()
messagebox.showinfo("成功", "已设置为活动配置组")
def save_config(self):
"""保存当前编辑的配置"""
# 检查索引有效性
if self.selected_index < 0 or self.selected_index >= len(config_manager.config['groups']):
messagebox.showerror("错误", f"请先选择一个有效的配置组")
return False
# 保存当前组的配置
group = config_manager.get_group_by_index(self.selected_index)
if not group:
messagebox.showerror("错误", f"配置组不存在")
return False
for key, var in self.config_vars.items():
if not key.startswith('display_'):
try:
value_str = var.get().strip() if var.get() else ""
# 特殊处理:某些字段需要转换为整数
if key in ['camera_index', 'camera_width', 'camera_height', 'serial_baudrate', 'move_velocity']:
try:
# 如果为空,使用当前值或默认值
if value_str:
value = int(value_str)
else:
# 如果下拉框为空,尝试从当前配置获取
value = group.get(key, 0)
except ValueError:
# 转换失败,使用当前值
value = group.get(key, 0)
print(f"⚠️ 字段 {key} 的值 '{value_str}' 无效,使用当前值 {value}")
else:
# 字符串字段
value = value_str if value_str else group.get(key, '')
group[key] = value
except Exception as e:
print(f"保存字段 {key} 时出错: {e}")
import traceback
traceback.print_exc()
# 保存失败时使用当前值
if key in group:
pass # 保持原值不变
# 保存预览配置
display = config_manager.config.get('display', {})
for key in ['preview_width', 'preview_height', 'preview_columns', 'preview_rows']:
var = self.config_vars.get(f"display_{key}")
if var:
try:
display[key] = int(var.get())
except:
pass
# 保存多窗口预览布尔值
display['preview_multi_window'] = bool(self.preview_multi_window_var.get())
# 保存预览全部配置组布尔值
display['preview_use_all_groups'] = bool(self.preview_use_all_groups_var.get())
config_manager.config['display'] = display
# 保存到文件
if config_manager.save_config():
# 更新左侧列表显示
self.update_group_list()
messagebox.showinfo("成功", "配置已保存")
return True
else:
messagebox.showerror("错误", "配置保存失败")
return False
def start_preview(self):
"""启动预览窗口(主线程,弹窗模式)"""
if not self.save_config_silent():
messagebox.showerror("错误", "配置保存失败,无法启动预览")
return
config_manager.load_config()
from preview import PreviewWindow
PreviewWindow().run(self.root) # 以主窗口为父Toplevel弹出独立预览窗
def save_config_silent(self):
"""静默保存配置(不显示消息框)"""
# 保存当前组的配置
if self.selected_index < 0 or self.selected_index >= len(config_manager.config['groups']):
print(f"⚠️ 选中索引 {self.selected_index} 无效")
return False
group = config_manager.get_group_by_index(self.selected_index)
if not group:
print(f"⚠️ 配置组不存在")
return False
for key, var in self.config_vars.items():
if not key.startswith('display_'):
try:
value_str = var.get().strip() if var.get() else ""
# 特殊处理:某些字段需要转换为整数
if key in ['camera_index', 'camera_width', 'camera_height', 'serial_baudrate', 'move_velocity']:
try:
# 如果为空,使用当前值或默认值
if value_str:
value = int(value_str)
else:
# 如果下拉框为空,尝试从当前配置获取
value = group.get(key, 0)
except ValueError:
# 转换失败,使用当前值
value = group.get(key, 0)
print(f"⚠️ 字段 {key} 的值 '{value_str}' 无效,使用当前值 {value}")
else:
# 字符串字段
value = value_str if value_str else group.get(key, '')
group[key] = value
except Exception as e:
print(f"保存字段 {key} 时出错: {e}")
import traceback
traceback.print_exc()
# 保存失败时保持原值不变
# 保存预览配置
display = config_manager.config.get('display', {})
for key in ['preview_width', 'preview_height', 'preview_columns', 'preview_rows']:
var = self.config_vars.get(f"display_{key}")
if var:
try:
display[key] = int(var.get())
except:
pass
# 保存多窗口预览布尔值(静默)
display['preview_multi_window'] = bool(self.preview_multi_window_var.get())
# 保存预览全部配置组布尔值(静默)
display['preview_use_all_groups'] = bool(self.preview_use_all_groups_var.get())
config_manager.config['display'] = display
# 保存到文件
result = config_manager.save_config()
# 更新左侧列表显示
if result:
self.update_group_list()
return result
def start_program(self):
"""启动单个配置组的主程序"""
# 保存配置(静默)
if not self.save_config_silent():
messagebox.showerror("错误", "配置保存失败")
return
# 检查是否有活动配置组
active_groups = [g for g in config_manager.config['groups'] if g.get('active', False)]
if not active_groups:
messagebox.showwarning("警告", "没有活动的配置组\n\n请先选择一个配置组并设置为活动")
return
# 启动单个配置组
import subprocess
import sys
try:
# 找到活动配置组的索引
active_group = active_groups[0]
group_index = config_manager.config['groups'].index(active_group)
subprocess.Popen([
sys.executable,
"main_single.py",
str(group_index)
], creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0)
messagebox.showinfo("成功", f"已启动配置组: {active_group['name']}\n\n请在控制台查看运行状态")
except Exception as e:
messagebox.showerror("错误", f"启动失败: {e}")
def start_multi_program(self):
"""启动多个配置组的主程序"""
# 保存配置(静默)
if not self.save_config_silent():
messagebox.showerror("错误", "配置保存失败")
return
# 启动多配置组管理器
import subprocess
import sys
try:
subprocess.Popen([
sys.executable,
"main_multi.py"
], creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0)
messagebox.showinfo("提示", "多配置组启动器已打开\n\n请在控制台中选择要启动的配置组")
except Exception as e:
messagebox.showerror("错误", f"启动失败: {e}")
def run(self):
"""运行GUI"""
self.root.mainloop()
if __name__ == "__main__":
app = ConfigGUI()
app.run()

79
launcher.py Normal file
View File

@@ -0,0 +1,79 @@
"""
启动器 - 选择启动配置界面或主程序
"""
import tkinter as tk
from tkinter import ttk
import subprocess
import sys
def start_config_gui():
"""启动配置界面"""
subprocess.Popen([sys.executable, "gui_config.py"])
def start_preview():
"""启动预览窗口"""
subprocess.Popen([sys.executable, "preview.py"])
def start_main_program():
"""启动主程序"""
subprocess.Popen([sys.executable, "main.py"])
def create_launcher():
"""创建启动器界面"""
root = tk.Tk()
root.title("火炬之光自动化 - 启动器")
root.geometry("400x580")
root.resizable(False, False)
# 标题
title_frame = tk.Frame(root)
title_frame.pack(fill=tk.X, pady=20)
tk.Label(title_frame, text="火炬之光自动化脚本", font=('Arial', 16, 'bold')).pack()
tk.Label(title_frame, text="请选择要启动的功能", font=('Arial', 10)).pack()
# 按钮区域
button_frame = tk.Frame(root)
button_frame.pack(fill=tk.BOTH, expand=True, padx=40, pady=20)
# 配置界面按钮
config_btn = tk.Button(button_frame, text="⚙️ 配置管理",
command=start_config_gui,
width=30, height=3, font=('Arial', 12))
config_btn.pack(pady=10)
# 预览窗口按钮
preview_btn = tk.Button(button_frame, text="👁️ 采集卡预览",
command=start_preview,
width=30, height=3, font=('Arial', 12))
preview_btn.pack(pady=10)
# 主程序按钮
main_btn = tk.Button(button_frame, text="🚀 启动单个配置组",
command=start_main_program,
width=30, height=2, font=('Arial', 11))
main_btn.pack(pady=5)
def start_multi_program():
"""启动多配置组程序"""
subprocess.Popen([sys.executable, "main_multi.py"],
creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0)
# 多配置组启动按钮
multi_btn = tk.Button(button_frame, text="🔥 启动多个配置组",
command=start_multi_program,
width=30, height=2, font=('Arial', 11))
multi_btn.pack(pady=5)
# 说明
info_label = tk.Label(root,
text="提示:首次使用请先点击配置管理设置参数",
font=('Arial', 9),
fg='gray')
info_label.pack(side=tk.BOTTOM, pady=10)
root.mainloop()
if __name__ == "__main__":
create_launcher()

226
main.py
View File

@@ -1,64 +1,95 @@
import cv2 import cv2
from utils.get_image import get_image from utils.get_image import GetImage
from utils.mouse import mouse_gui from utils.mouse import init_mouse_keyboard, Mouse_guiji
from ultralytics import YOLO from ultralytics import YOLO
import time import time
import serial import serial
import ch9329Comm import ch9329Comm
import time
import random import random
import math import math
from utils import shizi from utils import shizi
from config import config_manager
# 加载YOLO模型
model = YOLO(r"best.pt").to('cuda') model = YOLO(r"best.pt").to('cuda')
model0 = YOLO(r"best0.pt").to('cuda') model0 = YOLO(r"best0.pt").to('cuda')
# 从配置加载
active_group = config_manager.get_active_group()
if active_group is None:
print("❌ 没有活动的配置组请在gui_config.py中设置")
exit(1)
print(f"📋 使用配置组: {active_group['name']}")
# 初始化串口和鼠标
init_mouse_keyboard(active_group)
# 初始化键盘和鼠标
keyboard = ch9329Comm.keyboard.DataComm() keyboard = ch9329Comm.keyboard.DataComm()
mouse = ch9329Comm.mouse.DataComm(1920, 1080) from utils.mouse import mouse, mouse_gui # 导入已初始化的mouse和mouse_gui
kong_detections = {
'center': None, # 创建全局的mouse_gui实例
'next': None, mouse_gui = Mouse_guiji()
'npc1': None,
'npc2': None, # 初始化采集卡
'npc3': None, get_image = GetImage(
'npc4': None, cam_index=active_group['camera_index'],
'boss': None, width=active_group['camera_width'],
'daojv': [], height=active_group['camera_height']
'gw': [], )
'zhaozi': None
} # 检查采集卡是否初始化成功
left=0 if get_image.cap is None:
top=30 print(f"❌ 采集卡 {active_group['camera_index']} 初始化失败")
k=0#控制转圈的方向 print("请检查:")
panduan=False#是否在图内 print("1. 采集卡是否正确连接")
boss_pd=False#是否到boss关卡 print("2. 采集卡索引是否正确")
rw=(632,378) print("3. 采集卡驱动是否安装")
def yolo_shibie(im_PIL,detections,model): exit(1)
results = model(im_PIL)#目标检测
print(f"✅ 初始化完成 - 串口:{active_group['serial_port']} 采集卡:{active_group['camera_index']}")
# 全局变量
left = 0
top = 30
k = 0 # 控制转圈的方向
panduan = False # 是否在图内
boss_pd = False # 是否到boss关卡
rw = (632, 378)
# 从配置读取移动速度
v = active_group['move_velocity']
def yolo_shibie(im_PIL, detections, model):
results = model(im_PIL) # 目标检测
for result in results: for result in results:
for i in range(len(result.boxes.xyxy)): for i in range(len(result.boxes.xyxy)):
left, top, right, bottom = result.boxes.xyxy[i] left, top, right, bottom = result.boxes.xyxy[i]
scalar_tensor = result.boxes.cls[i] scalar_tensor = result.boxes.cls[i]
value = scalar_tensor.item() value = scalar_tensor.item()
label = result.names[int(value)] label = result.names[int(value)]
if label=='center'or label=='next' or label=='boss' or label=='zhaozi': if label == 'center' or label == 'next' or label == 'boss' or label == 'zhaozi':
player_x = int(left+(right-left)/2) player_x = int(left + (right - left) / 2)
player_y = int(top+(bottom-top)/2)+30 player_y = int(top + (bottom - top) / 2) + 30
RW = [player_x, player_y] RW = [player_x, player_y]
detections[label] = RW detections[label] = RW
elif label=='daojv' or label=='gw': elif label == 'daojv' or label == 'gw':
player_x = int(left + (right - left) / 2) player_x = int(left + (right - left) / 2)
player_y = int(top + (bottom - top) / 2) + 30 player_y = int(top + (bottom - top) / 2) + 30
RW = [player_x, player_y] RW = [player_x, player_y]
detections[label].append(RW) detections[label].append(RW)
elif label=='npc1' or label=='npc2' or label=='npc3' or label=='npc4': elif label == 'npc1' or label == 'npc2' or label == 'npc3' or label == 'npc4':
player_x = int(left+(right-left)/2) player_x = int(left + (right - left) / 2)
player_y = int(bottom)+30 player_y = int(bottom) + 30
RW = [player_x, player_y] RW = [player_x, player_y]
detections[label] = RW detections[label] = RW
return detections return detections
def sq(p1, p2): def sq(p1, p2):
"""计算两点之间的欧式距离""" """计算两点之间的欧式距离"""
return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
def process_points(points): def process_points(points):
if not points: if not points:
return None # 空列表情况 return None # 空列表情况
@@ -89,9 +120,10 @@ def process_points(points):
best_point = p best_point = p
return best_point return best_point
def move_randomly(rw, k): def move_randomly(rw, k):
k = k % 4 k = k % 4
suiji_t=float(random.randint(10,13)/10) suiji_t = float(random.randint(10, 13) / 10)
if k == 0: if k == 0:
keyboard.send_data("66") keyboard.send_data("66")
time.sleep(suiji_t) time.sleep(suiji_t)
@@ -109,18 +141,21 @@ def move_randomly(rw, k):
time.sleep(suiji_t) time.sleep(suiji_t)
keyboard.release() # Release keyboard.release() # Release
return k + 1 return k + 1
def move_to(rw,mb):
v=470 def move_to(rw, mb):
if rw[0]>=mb[0]: """使用配置的v值"""
global v
v = active_group['move_velocity'] # 每次都从配置读取最新值
if rw[0] >= mb[0]:
keyboard.send_data("44") keyboard.send_data("44")
time.sleep(float(abs(rw[0]-mb[0])/v)) time.sleep(float(abs(rw[0] - mb[0]) / v))
keyboard.release() # Release keyboard.release() # Release
else: else:
keyboard.send_data("66") keyboard.send_data("66")
time.sleep(float(abs(rw[0] - mb[0]) / v)) time.sleep(float(abs(rw[0] - mb[0]) / v))
keyboard.release() # Release keyboard.release() # Release
if rw[1]>=mb[1]: if rw[1] >= mb[1]:
keyboard.send_data("88") keyboard.send_data("88")
time.sleep(float(abs(rw[1] - mb[1]) / v)) time.sleep(float(abs(rw[1] - mb[1]) / v))
keyboard.release() # Release keyboard.release() # Release
@@ -128,7 +163,9 @@ def move_to(rw,mb):
keyboard.send_data("22") keyboard.send_data("22")
time.sleep(float(abs(rw[1] - mb[1]) / v)) time.sleep(float(abs(rw[1] - mb[1]) / v))
keyboard.release() keyboard.release()
i=0
i = 0
print("🚀 开始自动化...")
while True: while True:
detections = { detections = {
'center': None, 'center': None,
@@ -140,13 +177,21 @@ while True:
'boss': None, 'boss': None,
'daojv': [], 'daojv': [],
'gw': [], 'gw': [],
'zhaozi':None 'zhaozi': None
} }
im_opencv = get_image.get_frame()#[RGB,PIL] im_opencv = get_image.get_frame() # [RGB,PIL]
detections=yolo_shibie(im_opencv[1],detections,model) if im_opencv is None:
if shizi.tuwai(im_opencv[0]): # 进图算法 print("⚠️ 无法获取图像帧,跳过本次循环")
time.sleep(0.1)
continue
detections = yolo_shibie(im_opencv[1], detections, model)
if shizi.tuwai(im_opencv[0]): # 进图算法
im_opencv = get_image.get_frame() # [RGB,PIL] im_opencv = get_image.get_frame() # [RGB,PIL]
if im_opencv is None:
print("⚠️ 无法获取图像帧,跳过本次循环")
time.sleep(0.1)
continue
detections = yolo_shibie(im_opencv[1], detections, model0) detections = yolo_shibie(im_opencv[1], detections, model0)
print('当前在城镇中') print('当前在城镇中')
if detections['npc1'] is not None and sq(rw, detections['npc1']) > 80: if detections['npc1'] is not None and sq(rw, detections['npc1']) > 80:
@@ -162,7 +207,7 @@ while True:
continue continue
elif detections['npc3'] is not None and detections['npc4'] is None: elif detections['npc3'] is not None and detections['npc4'] is None:
print("在npc3旁边向右走") print("在npc3旁边向右走")
mb = (rw[0], detections['npc3'][1]-50) mb = (rw[0], detections['npc3'][1] - 50)
move_to(rw, mb) move_to(rw, mb)
mb = (rw[0] + 700, rw[1]) mb = (rw[0] + 700, rw[1])
move_to(rw, mb) move_to(rw, mb)
@@ -175,59 +220,58 @@ while True:
keyboard.release() keyboard.release()
time.sleep(1) time.sleep(1)
im_opencv = get_image.get_frame() # [RGB,PIL] im_opencv = get_image.get_frame() # [RGB,PIL]
if im_opencv is None:
print("⚠️ 无法获取图像帧")
continue
if shizi.daoying(im_opencv[0]): if shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1) mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1) time.sleep(1)
continue continue
else: else:
print("离npc4有点远 点击进入") print("离npc4有点远 点击进入")
move_to(rw, detections['npc4']) move_to(rw, detections['npc4'])
time.sleep(1) time.sleep(1)
im_opencv = get_image.get_frame() # [RGB,PIL] im_opencv = get_image.get_frame() # [RGB,PIL]
if im_opencv is None:
print("⚠️ 无法获取图像帧")
continue
if shizi.daoying(im_opencv[0]): if shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1) mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1) time.sleep(1)
continue continue
elif shizi.tiaozhan(im_opencv[0]):#开启挑战 elif shizi.tiaozhan(im_opencv[0]): # 开启挑战
print('进入塔4') print('进入塔4')
mouse_gui.send_data_absolute(left+1100,top+600,may=1) mouse_gui.send_data_absolute(left + 1100, top + 600, may=1)
time.sleep(0.3) time.sleep(0.3)
mouse_gui.send_data_absolute(left + 433, top + 455,may=1) mouse_gui.send_data_absolute(left + 433, top + 455, may=1)
panduan = True panduan = True
continue continue
elif shizi.jieshu(im_opencv[0]):#结束挑战 elif shizi.jieshu(im_opencv[0]): # 结束挑战
print('结束挑战') print('结束挑战')
mouse_gui.send_data_absolute(left+542,top+644,may=1) mouse_gui.send_data_absolute(left + 542, top + 644, may=1)
time.sleep(0.8) time.sleep(0.8)
mouse_gui.send_data_absolute(left + 706, top + 454,may=1) mouse_gui.send_data_absolute(left + 706, top + 454, may=1)
continue continue
elif panduan :#图内情况 elif panduan: # 图内情况
print("在图内") print("在图内")
if shizi.shuzi(im_opencv[0]) : if shizi.shuzi(im_opencv[0]):
boss_pd = True boss_pd = True
print("进入到boss") print("进入到boss")
if shizi.fuhuo(im_opencv[0]): if shizi.fuhuo(im_opencv[0]):
print('点击复活') print('点击复活')
mouse_gui.send_data_absolute(left + 536, top + 627, may=1) mouse_gui.send_data_absolute(left + 536, top + 627, may=1)
mouse_gui.send_data_absolute(rw[0], rw[1], may=0) time.sleep(0.15)
keyboard.release()
continue continue
if detections['zhaozi'] is not None: if shizi.tuichu(im_opencv[0]) and detections['next'] is None and len(detections['daojv']) == 0 and len(detections['gw']) == 0 and boss_pd:
move_to(rw,detections['zhaozi'])
continue
if len(detections['daojv'])!=0:
move_to(rw,process_points(detections['daojv']))
for i in range(3+len(detections['daojv'])):
keyboard.send_data("AA")
time.sleep(0.15)
keyboard.release()
continue
if shizi.tuichu(im_opencv[0]) and detections['next'] is None and len(detections['daojv'])==0 and len(detections['gw'])==0 and boss_pd:
print("识别到可以退出挑战!!!!!!!!!!!!!!!!!!") print("识别到可以退出挑战!!!!!!!!!!!!!!!!!!")
for i in range(3): for i in range(3):
time.sleep(0.5) time.sleep(0.5)
im_opencv = get_image.get_frame()#[RGB,PIL] im_opencv = get_image.get_frame() # [RGB,PIL]
if im_opencv is None:
print("⚠️ 无法获取图像帧")
continue
detections = { detections = {
'center': None, 'center': None,
'next': None, 'next': None,
@@ -239,52 +283,49 @@ while True:
'daojv': [], 'daojv': [],
'gw': [], 'gw': [],
'zhaozi': None 'zhaozi': None
} }
detections = yolo_shibie(im_opencv[1], detections,model) detections = yolo_shibie(im_opencv[1], detections, model)
if detections['next'] is not None or len(detections['daojv'])!=0 or len(detections['gw'])!=0 or detections['boss'] is not None: if detections['next'] is not None or len(detections['daojv']) != 0 or len(detections['gw']) != 0 or detections['boss'] is not None:
break break
else: else:
mouse_gui.send_data_absolute(left + 640, top + 40,may=1)#点击退出 mouse_gui.send_data_absolute(left + 640, top + 40, may=1) # 点击退出
panduan = False#退出挑战 panduan = False # 退出挑战
boss_pd = False boss_pd = False
time.sleep(2.0) time.sleep(2.0)
continue continue
if detections['center'] is None and detections['next'] is None and (len(detections['gw'])!=0 or detections["boss"] is not None):#识别不到中心情况 但是有怪物 if detections['center'] is None and detections['next'] is None and (len(detections['gw']) != 0 or detections["boss"] is not None): # 识别不到中心情况 但是有怪物
print("未检测到中心点,但是有怪物") print("未检测到中心点,但是有怪物")
if len(detections['gw'])!=0: if len(detections['gw']) != 0:
move_to(rw,detections['gw'][0]) move_to(rw, detections['gw'][0])
time.sleep(0.26) time.sleep(0.26)
elif detections['boss'] is not None:#跟随boss elif detections['boss'] is not None: # 跟随boss
boss_suiji1=random.randint(-30,30) boss_suiji1 = random.randint(-30, 30)
boss_suiji2 = random.randint(-30, 30) boss_suiji2 = random.randint(-30, 30)
detections['boss']=(detections['boss'][0]+boss_suiji1,detections['boss'][1]+boss_suiji2) detections['boss'] = (detections['boss'][0] + boss_suiji1, detections['boss'][1] + boss_suiji2)
move_to(rw,detections['boss']) move_to(rw, detections['boss'])
time.sleep(0.7) time.sleep(0.7)
continue continue
elif (detections['center'] is not None and detections['next'] is None and len(detections['gw'])!=0) or (boss_pd==True and detections['center'] is not None and detections['next'] is None) :#识别到中心 但是有怪物 elif (detections['center'] is not None and detections['next'] is None and len(detections['gw']) != 0) or (boss_pd == True and detections['center'] is not None and detections['next'] is None): # 识别到中心 但是有怪物
if detections['center'][0]>=rw[0] and detections['center'][1]<rw[1]:#3 if detections['center'][0] >= rw[0] and detections['center'][1] < rw[1]: # 3
mb=(rw[0]+100,rw[1]) mb = (rw[0] + 100, rw[1])
elif detections['center'][0]<=rw[0] and detections['center'][1]<rw[1]:#4 elif detections['center'][0] <= rw[0] and detections['center'][1] < rw[1]: # 4
mb=(rw[0], rw[1]-100) mb = (rw[0], rw[1] - 100)
elif detections['center'][0]<=rw[0] and detections['center'][1]>rw[1]:#1 elif detections['center'][0] <= rw[0] and detections['center'][1] > rw[1]: # 1
mb=(rw[0]-100, rw[1]) mb = (rw[0] - 100, rw[1])
elif detections['center'][0]>=rw[0] and detections['center'][1]>rw[1]:#2 elif detections['center'][0] >= rw[0] and detections['center'][1] > rw[1]: # 2
mb=(rw[0], rw[1]+100) mb = (rw[0], rw[1] + 100)
move_to(rw, mb) move_to(rw, mb)
time.sleep(0.25) time.sleep(0.25)
continue continue
elif boss_pd==True and detections['center'] is None and detections['next'] is None:#boss出现了 但是没有中心 elif boss_pd == True and detections['center'] is None and detections['next'] is None: # boss出现了 但是没有中心
k=move_randomly(rw,k) k = move_randomly(rw, k)
continue continue
elif detections['next'] is not None: elif detections['next'] is not None:
print('进入下一层啦啦啦啦啦啦') print('进入下一层啦啦啦啦啦啦')
panduan = True panduan = True
move_to(rw,detections['next']) move_to(rw, detections['next'])
for i in range(2): for i in range(2):
keyboard.send_data("DD") keyboard.send_data("DD")
time.sleep(0.15) time.sleep(0.15)
@@ -298,3 +339,4 @@ while True:
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1) mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1) time.sleep(1)
continue continue

163
main_multi.py Normal file
View File

@@ -0,0 +1,163 @@
"""
多配置组启动器
支持同时启动多个配置组的自动化程序
"""
import multiprocessing
import sys
from config import config_manager
from main_single import run_automation_for_group
from utils.logger import logger, throttle
import logging
import time
def main():
"""主函数"""
# 重新加载配置
config_manager.load_config()
# 获取所有配置组
groups = config_manager.config.get('groups', [])
if not groups:
logger.error("❌ 没有找到任何配置组")
logger.info("请先运行 gui_config.py 创建配置组")
return
# 询问要启动哪些配置组
logger.info("=" * 60)
logger.info("🔥 多配置组启动器")
logger.info("=" * 60)
logger.info("\n可用配置组:")
for i, group in enumerate(groups):
active_mark = "" if group.get('active', False) else " "
logger.info(f" [{i}] {active_mark} {group['name']}")
logger.info(f" 串口: {group['serial_port']} | 采集卡: {group['camera_index']}")
logger.info("\n选择启动方式:")
logger.info(" 1. 启动所有活动配置组")
logger.info(" 2. 启动所有配置组")
logger.info(" 3. 选择特定配置组")
logger.info(" 0. 退出")
choice = input("\n请选择 (0-3): ").strip()
selected_indices = []
if choice == "0":
logger.info("👋 退出")
return
elif choice == "1":
# 启动所有活动配置组
selected_indices = [i for i, g in enumerate(groups) if g.get('active', False)]
if not selected_indices:
logger.error("❌ 没有活动的配置组")
return
logger.info(f"\n✅ 将启动 {len(selected_indices)} 个活动配置组")
elif choice == "2":
# 启动所有配置组
selected_indices = list(range(len(groups)))
logger.info(f"\n✅ 将启动所有 {len(selected_indices)} 个配置组")
elif choice == "3":
# 选择特定配置组
indices_input = input("请输入要启动的配置组索引(用逗号分隔,如: 0,1,2): ").strip()
try:
selected_indices = [int(x.strip()) for x in indices_input.split(',')]
# 验证索引有效性
selected_indices = [i for i in selected_indices if 0 <= i < len(groups)]
if not selected_indices:
logger.error("❌ 没有有效的配置组索引")
return
logger.info(f"\n✅ 将启动 {len(selected_indices)} 个配置组")
except ValueError:
logger.error("❌ 输入格式错误")
return
else:
logger.error("❌ 无效选择")
return
# 显示将要启动的配置组
logger.info("\n将要启动的配置组:")
for idx in selected_indices:
group = groups[idx]
logger.info(f"{group['name']} (串口:{group['serial_port']}, 采集卡:{group['camera_index']})")
# 串口冲突预检:同一串口被多个组占用通常会导致仅一路成功
port_to_groups = {}
for idx in selected_indices:
g = groups[idx]
port_to_groups.setdefault(g['serial_port'], []).append(g['name'])
conflicts = {p: names for p, names in port_to_groups.items() if p and len(names) > 1}
if conflicts:
logger.warning("⚠️ 检测到串口冲突同一COM被多个组使用")
for p, names in conflicts.items():
logger.warning(f" {p}: {', '.join(names)}")
go_on = input("上述冲突很可能导致仅一组成功,其它失败。仍要继续? (y/n): ").strip().lower()
if go_on != 'y':
logger.info("已取消启动以避免串口冲突")
return
confirm = input("\n确认启动? (y/n): ").strip().lower()
if confirm != 'y':
logger.info("❌ 取消启动")
return
# 启动多进程
processes = []
for idx in selected_indices:
group = groups[idx]
logger.info(f"启动进程: {group['name']}...")
process = multiprocessing.Process(
target=run_automation_for_group,
args=(idx,),
name=f"Group-{idx}-{group['name']}"
)
process.start()
processes.append((idx, group['name'], process))
logger.info(f"{group['name']} 已启动 (PID: {process.pid})")
logger.info(f"\n✅ 成功启动 {len(processes)} 个配置组进程")
logger.info("\n" + "=" * 60)
logger.info("运行状态:")
logger.info("=" * 60)
# 监控进程状态
try:
while True:
alive_count = 0
for idx, name, proc in processes:
if proc.is_alive():
alive_count += 1
else:
logger.warning(f"⚠️ {name} 进程已退出 (退出码: {proc.exitcode})")
if alive_count == 0:
logger.info("\n所有进程已退出")
break
time.sleep(2)
# 打印存活状态
alive_names = [name for idx, name, proc in processes if proc.is_alive()]
if alive_names:
# 每2秒节流一次状态行
throttle("multi_alive", 2.0, logging.INFO, f"📊 运行中: {', '.join(alive_names)} ({alive_count}/{len(processes)})")
except KeyboardInterrupt:
logger.warning("\n\n🛑 收到停止信号,正在关闭所有进程...")
for idx, name, proc in processes:
if proc.is_alive():
logger.info(f"正在停止 {name}...")
proc.terminate()
proc.join(timeout=5)
if proc.is_alive():
logger.warning(f"强制停止 {name}...")
proc.kill()
logger.info(f"{name} 已停止")
logger.info("\n👋 所有进程已停止")
if __name__ == "__main__":
multiprocessing.freeze_support() # Windows下需要
main()

325
main_new.py Normal file
View File

@@ -0,0 +1,325 @@
import cv2
from utils.get_image import GetImage
from utils.mouse import init_mouse_keyboard, Mouse_guiji
from ultralytics import YOLO
import time
import serial
import ch9329Comm
import random
import math
from utils import shizi
from config import config_manager
# 加载YOLO模型
model = YOLO(r"best.pt").to('cuda')
model0 = YOLO(r"best0.pt").to('cuda')
# 从配置加载
active_group = config_manager.get_active_group()
if active_group is None:
print("❌ 没有活动的配置组请在gui_config.py中设置")
exit(1)
print(f"📋 使用配置组: {active_group['name']}")
# 初始化串口和鼠标
init_mouse_keyboard(active_group)
# 初始化键盘和鼠标
keyboard = ch9329Comm.keyboard.DataComm()
from utils.mouse import mouse, mouse_gui # 导入已初始化的mouse和mouse_gui
# 创建全局的mouse_gui实例
mouse_gui = Mouse_guiji()
# 初始化采集卡
get_image = GetImage(
cam_index=active_group['camera_index'],
width=active_group['camera_width'],
height=active_group['camera_height']
)
print(f"✅ 初始化完成 - 串口:{active_group['serial_port']} 采集卡:{active_group['camera_index']}")
# 全局变量
left = 0
top = 30
k = 0 # 控制转圈的方向
panduan = False # 是否在图内
boss_pd = False # 是否到boss关卡
rw = (632, 378)
# 从配置读取移动速度
v = active_group['move_velocity']
def yolo_shibie(im_PIL, detections, model):
results = model(im_PIL) # 目标检测
for result in results:
for i in range(len(result.boxes.xyxy)):
left, top, right, bottom = result.boxes.xyxy[i]
scalar_tensor = result.boxes.cls[i]
value = scalar_tensor.item()
label = result.names[int(value)]
if label == 'center' or label == 'next' or label == 'boss' or label == 'zhaozi':
player_x = int(left + (right - left) / 2)
player_y = int(top + (bottom - top) / 2) + 30
RW = [player_x, player_y]
detections[label] = RW
elif label == 'daojv' or label == 'gw':
player_x = int(left + (right - left) / 2)
player_y = int(top + (bottom - top) / 2) + 30
RW = [player_x, player_y]
detections[label].append(RW)
elif label == 'npc1' or label == 'npc2' or label == 'npc3' or label == 'npc4':
player_x = int(left + (right - left) / 2)
player_y = int(bottom) + 30
RW = [player_x, player_y]
detections[label] = RW
return detections
def sq(p1, p2):
"""计算两点之间的欧式距离"""
return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
def process_points(points):
if not points:
return None # 空列表情况
n = len(points)
if n == 1:
# 只有一个点,直接返回
return points[0]
elif n == 2:
# 两个点取中点
x = (points[0][0] + points[1][0]) / 2
y = (points[0][1] + points[1][1]) / 2
return [x, y]
else:
# 随机选3个点
sample_points = random.sample(points, 3)
# 对每个点计算到这3个点的总距离
min_sum = float('inf')
best_point = None
for p in points:
dist_sum = sum(sq(p, sp) for sp in sample_points)
if dist_sum < min_sum:
min_sum = dist_sum
best_point = p
return best_point
def move_randomly(rw, k):
k = k % 4
suiji_t = float(random.randint(10, 13) / 10)
if k == 0:
keyboard.send_data("66")
time.sleep(suiji_t)
keyboard.release() # Release
elif k == 1:
keyboard.send_data("88")
time.sleep(suiji_t)
keyboard.release() # Release
elif k == 2:
keyboard.send_data("44")
time.sleep(suiji_t)
keyboard.release() # Release
elif k == 3:
keyboard.send_data("22")
time.sleep(suiji_t)
keyboard.release() # Release
return k + 1
def move_to(rw, mb):
"""使用配置的v值"""
global v
v = active_group['move_velocity'] # 每次都从配置读取最新值
if rw[0] >= mb[0]:
keyboard.send_data("44")
time.sleep(float(abs(rw[0] - mb[0]) / v))
keyboard.release() # Release
else:
keyboard.send_data("66")
time.sleep(float(abs(rw[0] - mb[0]) / v))
keyboard.release() # Release
if rw[1] >= mb[1]:
keyboard.send_data("88")
time.sleep(float(abs(rw[1] - mb[1]) / v))
keyboard.release() # Release
else:
keyboard.send_data("22")
time.sleep(float(abs(rw[1] - mb[1]) / v))
keyboard.release()
i = 0
print("🚀 开始自动化...")
while True:
detections = {
'center': None,
'next': None,
'npc1': None,
'npc2': None,
'npc3': None,
'npc4': None,
'boss': None,
'daojv': [],
'gw': [],
'zhaozi': None
}
im_opencv = get_image.get_frame() # [RGB,PIL]
detections = yolo_shibie(im_opencv[1], detections, model)
if shizi.tuwai(im_opencv[0]): # 进图算法
im_opencv = get_image.get_frame() # [RGB,PIL]
detections = yolo_shibie(im_opencv[1], detections, model0)
print('当前在城镇中')
if detections['npc1'] is not None and sq(rw, detections['npc1']) > 80:
print("向npc1靠近")
print(sq(rw, detections['npc1']))
move_to(rw, detections['npc1'])
continue
elif detections['npc1'] is not None and sq(rw, detections['npc1']) <= 80:
print("在npc旁边向上走")
print(sq(rw, detections['npc1']))
mb = (detections['npc1'][0], detections['npc1'][1] - 1010)
move_to(rw, mb)
continue
elif detections['npc3'] is not None and detections['npc4'] is None:
print("在npc3旁边向右走")
mb = (rw[0], detections['npc3'][1] - 50)
move_to(rw, mb)
mb = (rw[0] + 700, rw[1])
move_to(rw, mb)
continue
elif detections['npc4'] is not None:
if sq(detections['npc4'], rw) < 50:
print("离npc4很近 直接进入")
keyboard.send_data("DD")
time.sleep(0.15)
keyboard.release()
time.sleep(1)
im_opencv = get_image.get_frame() # [RGB,PIL]
if shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1)
continue
else:
print("离npc4有点远 点击进入")
move_to(rw, detections['npc4'])
time.sleep(1)
im_opencv = get_image.get_frame() # [RGB,PIL]
if shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1)
continue
elif shizi.tiaozhan(im_opencv[0]): # 开启挑战
print('进入塔4')
mouse_gui.send_data_absolute(left + 1100, top + 600, may=1)
time.sleep(0.3)
mouse_gui.send_data_absolute(left + 433, top + 455, may=1)
panduan = True
continue
elif shizi.jieshu(im_opencv[0]): # 结束挑战
print('结束挑战')
mouse_gui.send_data_absolute(left + 542, top + 644, may=1)
time.sleep(0.8)
mouse_gui.send_data_absolute(left + 706, top + 454, may=1)
continue
elif panduan: # 图内情况
print("在图内")
if shizi.shuzi(im_opencv[0]):
boss_pd = True
print("进入到boss")
if shizi.fuhuo(im_opencv[0]):
print('点击复活')
mouse_gui.send_data_absolute(left + 536, top + 627, may=1)
mouse_gui.send_data_absolute(rw[0], rw[1], may=0)
continue
if detections['zhaozi'] is not None:
move_to(rw, detections['zhaozi'])
continue
if len(detections['daojv']) != 0:
move_to(rw, process_points(detections['daojv']))
for i in range(3 + len(detections['daojv'])):
keyboard.send_data("AA")
time.sleep(0.15)
keyboard.release()
continue
if shizi.tuichu(im_opencv[0]) and detections['next'] is None and len(detections['daojv']) == 0 and len(detections['gw']) == 0 and boss_pd:
print("识别到可以退出挑战!!!!!!!!!!!!!!!!!!")
for i in range(3):
time.sleep(0.5)
im_opencv = get_image.get_frame() # [RGB,PIL]
detections = {
'center': None,
'next': None,
'npc1': None,
'npc2': None,
'npc3': None,
'npc4': None,
'boss': None,
'daojv': [],
'gw': [],
'zhaozi': None
}
detections = yolo_shibie(im_opencv[1], detections, model)
if detections['next'] is not None or len(detections['daojv']) != 0 or len(detections['gw']) != 0 or detections['boss'] is not None:
break
else:
mouse_gui.send_data_absolute(left + 640, top + 40, may=1) # 点击退出
panduan = False # 退出挑战
boss_pd = False
time.sleep(2.0)
continue
if detections['center'] is None and detections['next'] is None and (len(detections['gw']) != 0 or detections["boss"] is not None): # 识别不到中心情况 但是有怪物
print("未检测到中心点,但是有怪物")
if len(detections['gw']) != 0:
move_to(rw, detections['gw'][0])
time.sleep(0.26)
elif detections['boss'] is not None: # 跟随boss
boss_suiji1 = random.randint(-30, 30)
boss_suiji2 = random.randint(-30, 30)
detections['boss'] = (detections['boss'][0] + boss_suiji1, detections['boss'][1] + boss_suiji2)
move_to(rw, detections['boss'])
time.sleep(0.7)
continue
elif (detections['center'] is not None and detections['next'] is None and len(detections['gw']) != 0) or (boss_pd == True and detections['center'] is not None and detections['next'] is None): # 识别到中心 但是有怪物
if detections['center'][0] >= rw[0] and detections['center'][1] < rw[1]: # 3
mb = (rw[0] + 100, rw[1])
elif detections['center'][0] <= rw[0] and detections['center'][1] < rw[1]: # 4
mb = (rw[0], rw[1] - 100)
elif detections['center'][0] <= rw[0] and detections['center'][1] > rw[1]: # 1
mb = (rw[0] - 100, rw[1])
elif detections['center'][0] >= rw[0] and detections['center'][1] > rw[1]: # 2
mb = (rw[0], rw[1] + 100)
move_to(rw, mb)
time.sleep(0.25)
continue
elif boss_pd == True and detections['center'] is None and detections['next'] is None: # boss出现了 但是没有中心
k = move_randomly(rw, k)
continue
elif detections['next'] is not None:
print('进入下一层啦啦啦啦啦啦')
panduan = True
move_to(rw, detections['next'])
for i in range(2):
keyboard.send_data("DD")
time.sleep(0.15)
keyboard.release()
continue
else:
k = move_randomly(rw, k)
continue
elif shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1)
continue

300
main_old.py Normal file
View File

@@ -0,0 +1,300 @@
import cv2
from utils.get_image import get_image
from utils.mouse import mouse_gui
from ultralytics import YOLO
import time
import serial
import ch9329Comm
import time
import random
import math
from utils import shizi
model = YOLO(r"best.pt").to('cuda')
model0 = YOLO(r"best0.pt").to('cuda')
keyboard = ch9329Comm.keyboard.DataComm()
mouse = ch9329Comm.mouse.DataComm(1920, 1080)
kong_detections = {
'center': None,
'next': None,
'npc1': None,
'npc2': None,
'npc3': None,
'npc4': None,
'boss': None,
'daojv': [],
'gw': [],
'zhaozi': None
}
left=0
top=30
k=0#控制转圈的方向
panduan=False#是否在图内
boss_pd=False#是否到boss关卡
rw=(632,378)
def yolo_shibie(im_PIL,detections,model):
results = model(im_PIL)#目标检测
for result in results:
for i in range(len(result.boxes.xyxy)):
left, top, right, bottom = result.boxes.xyxy[i]
scalar_tensor = result.boxes.cls[i]
value = scalar_tensor.item()
label = result.names[int(value)]
if label=='center'or label=='next' or label=='boss' or label=='zhaozi':
player_x = int(left+(right-left)/2)
player_y = int(top+(bottom-top)/2)+30
RW = [player_x, player_y]
detections[label] = RW
elif label=='daojv' or label=='gw':
player_x = int(left + (right - left) / 2)
player_y = int(top + (bottom - top) / 2) + 30
RW = [player_x, player_y]
detections[label].append(RW)
elif label=='npc1' or label=='npc2' or label=='npc3' or label=='npc4':
player_x = int(left+(right-left)/2)
player_y = int(bottom)+30
RW = [player_x, player_y]
detections[label] = RW
return detections
def sq(p1, p2):
"""计算两点之间的欧式距离"""
return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
def process_points(points):
if not points:
return None # 空列表情况
n = len(points)
if n == 1:
# 只有一个点,直接返回
return points[0]
elif n == 2:
# 两个点取中点
x = (points[0][0] + points[1][0]) / 2
y = (points[0][1] + points[1][1]) / 2
return [x, y]
else:
# 随机选3个点
sample_points = random.sample(points, 3)
# 对每个点计算到这3个点的总距离
min_sum = float('inf')
best_point = None
for p in points:
dist_sum = sum(sq(p, sp) for sp in sample_points)
if dist_sum < min_sum:
min_sum = dist_sum
best_point = p
return best_point
def move_randomly(rw, k):
k = k % 4
suiji_t=float(random.randint(10,13)/10)
if k == 0:
keyboard.send_data("66")
time.sleep(suiji_t)
keyboard.release() # Release
elif k == 1:
keyboard.send_data("88")
time.sleep(suiji_t)
keyboard.release() # Release
elif k == 2:
keyboard.send_data("44")
time.sleep(suiji_t)
keyboard.release() # Release
elif k == 3:
keyboard.send_data("22")
time.sleep(suiji_t)
keyboard.release() # Release
return k + 1
def move_to(rw,mb):
v=470
if rw[0]>=mb[0]:
keyboard.send_data("44")
time.sleep(float(abs(rw[0]-mb[0])/v))
keyboard.release() # Release
else:
keyboard.send_data("66")
time.sleep(float(abs(rw[0] - mb[0]) / v))
keyboard.release() # Release
if rw[1]>=mb[1]:
keyboard.send_data("88")
time.sleep(float(abs(rw[1] - mb[1]) / v))
keyboard.release() # Release
else:
keyboard.send_data("22")
time.sleep(float(abs(rw[1] - mb[1]) / v))
keyboard.release()
i=0
while True:
detections = {
'center': None,
'next': None,
'npc1': None,
'npc2': None,
'npc3': None,
'npc4': None,
'boss': None,
'daojv': [],
'gw': [],
'zhaozi':None
}
im_opencv = get_image.get_frame()#[RGB,PIL]
detections=yolo_shibie(im_opencv[1],detections,model)
if shizi.tuwai(im_opencv[0]): # 进图算法
im_opencv = get_image.get_frame() # [RGB,PIL]
detections = yolo_shibie(im_opencv[1], detections, model0)
print('当前在城镇中')
if detections['npc1'] is not None and sq(rw, detections['npc1']) > 80:
print("向npc1靠近")
print(sq(rw, detections['npc1']))
move_to(rw, detections['npc1'])
continue
elif detections['npc1'] is not None and sq(rw, detections['npc1']) <= 80:
print("在npc旁边向上走")
print(sq(rw, detections['npc1']))
mb = (detections['npc1'][0], detections['npc1'][1] - 1010)
move_to(rw, mb)
continue
elif detections['npc3'] is not None and detections['npc4'] is None:
print("在npc3旁边向右走")
mb = (rw[0], detections['npc3'][1]-50)
move_to(rw, mb)
mb = (rw[0] + 700, rw[1])
move_to(rw, mb)
continue
elif detections['npc4'] is not None:
if sq(detections['npc4'], rw) < 50:
print("离npc4很近 直接进入")
keyboard.send_data("DD")
time.sleep(0.15)
keyboard.release()
time.sleep(1)
im_opencv = get_image.get_frame() # [RGB,PIL]
if shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1)
continue
else:
print("离npc4有点远 点击进入")
move_to(rw, detections['npc4'])
time.sleep(1)
im_opencv = get_image.get_frame() # [RGB,PIL]
if shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1)
continue
elif shizi.tiaozhan(im_opencv[0]):#开启挑战
print('进入塔4')
mouse_gui.send_data_absolute(left+1100,top+600,may=1)
time.sleep(0.3)
mouse_gui.send_data_absolute(left + 433, top + 455,may=1)
panduan = True
continue
elif shizi.jieshu(im_opencv[0]):#结束挑战
print('结束挑战')
mouse_gui.send_data_absolute(left+542,top+644,may=1)
time.sleep(0.8)
mouse_gui.send_data_absolute(left + 706, top + 454,may=1)
continue
elif panduan :#图内情况
print("在图内")
if shizi.shuzi(im_opencv[0]) :
boss_pd = True
print("进入到boss")
if shizi.fuhuo(im_opencv[0]):
print('点击复活')
mouse_gui.send_data_absolute(left + 536, top + 627, may=1)
mouse_gui.send_data_absolute(rw[0], rw[1], may=0)
continue
if detections['zhaozi'] is not None:
move_to(rw,detections['zhaozi'])
continue
if len(detections['daojv'])!=0:
move_to(rw,process_points(detections['daojv']))
for i in range(3+len(detections['daojv'])):
keyboard.send_data("AA")
time.sleep(0.15)
keyboard.release()
continue
if shizi.tuichu(im_opencv[0]) and detections['next'] is None and len(detections['daojv'])==0 and len(detections['gw'])==0 and boss_pd:
print("识别到可以退出挑战!!!!!!!!!!!!!!!!!!")
for i in range(3):
time.sleep(0.5)
im_opencv = get_image.get_frame()#[RGB,PIL]
detections = {
'center': None,
'next': None,
'npc1': None,
'npc2': None,
'npc3': None,
'npc4': None,
'boss': None,
'daojv': [],
'gw': [],
'zhaozi': None
}
detections = yolo_shibie(im_opencv[1], detections,model)
if detections['next'] is not None or len(detections['daojv'])!=0 or len(detections['gw'])!=0 or detections['boss'] is not None:
break
else:
mouse_gui.send_data_absolute(left + 640, top + 40,may=1)#点击退出
panduan = False#退出挑战
boss_pd = False
time.sleep(2.0)
continue
if detections['center'] is None and detections['next'] is None and (len(detections['gw'])!=0 or detections["boss"] is not None):#识别不到中心情况 但是有怪物
print("未检测到中心点,但是有怪物")
if len(detections['gw'])!=0:
move_to(rw,detections['gw'][0])
time.sleep(0.26)
elif detections['boss'] is not None:#跟随boss
boss_suiji1=random.randint(-30,30)
boss_suiji2 = random.randint(-30, 30)
detections['boss']=(detections['boss'][0]+boss_suiji1,detections['boss'][1]+boss_suiji2)
move_to(rw,detections['boss'])
time.sleep(0.7)
continue
elif (detections['center'] is not None and detections['next'] is None and len(detections['gw'])!=0) or (boss_pd==True and detections['center'] is not None and detections['next'] is None) :#识别到中心 但是有怪物
if detections['center'][0]>=rw[0] and detections['center'][1]<rw[1]:#3
mb=(rw[0]+100,rw[1])
elif detections['center'][0]<=rw[0] and detections['center'][1]<rw[1]:#4
mb=(rw[0], rw[1]-100)
elif detections['center'][0]<=rw[0] and detections['center'][1]>rw[1]:#1
mb=(rw[0]-100, rw[1])
elif detections['center'][0]>=rw[0] and detections['center'][1]>rw[1]:#2
mb=(rw[0], rw[1]+100)
move_to(rw, mb)
time.sleep(0.25)
continue
elif boss_pd==True and detections['center'] is None and detections['next'] is None:#boss出现了 但是没有中心
k=move_randomly(rw,k)
continue
elif detections['next'] is not None:
print('进入下一层啦啦啦啦啦啦')
panduan = True
move_to(rw,detections['next'])
for i in range(2):
keyboard.send_data("DD")
time.sleep(0.15)
keyboard.release()
continue
else:
k = move_randomly(rw, k)
continue
elif shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1)
continue

349
main_single.py Normal file
View File

@@ -0,0 +1,349 @@
"""
单配置组运行的主程序
用于在独立进程中运行单个配置组的自动化
"""
import cv2
from utils.get_image import GetImage
from utils.mouse import init_mouse_keyboard, Mouse_guiji
from ultralytics import YOLO
import time
import serial
import ch9329Comm
import random
import math
from utils import shizi
from config import config_manager
import sys
def run_automation_for_group(group_index):
"""为单个配置组运行自动化"""
# 重新加载配置
config_manager.load_config()
group = config_manager.get_group_by_index(group_index)
if group is None:
print(f"❌ 配置组索引 {group_index} 不存在")
return
print(f"🚀 启动配置组: {group['name']} (索引: {group_index})")
print(f" 串口: {group['serial_port']}")
print(f" 采集卡: {group['camera_index']}")
# 加载YOLO模型
model = YOLO(r"best.pt").to('cuda')
model0 = YOLO(r"best0.pt").to('cuda')
# 初始化串口和鼠标
try:
init_mouse_keyboard(group)
except Exception as e:
print(f"❌ 初始化串口失败: {e}")
return
# 初始化键盘和鼠标
keyboard = ch9329Comm.keyboard.DataComm()
from utils.mouse import mouse
mouse_gui = Mouse_guiji()
# 初始化采集卡
get_image = GetImage(
cam_index=group['camera_index'],
width=group['camera_width'],
height=group['camera_height']
)
# 检查采集卡是否初始化成功
if get_image.cap is None:
print(f"❌ 采集卡 {group['camera_index']} 初始化失败")
return
print(f"✅ 配置组 {group['name']} 初始化完成")
# 全局变量
left = 0
top = 30
k = 0 # 控制转圈的方向
panduan = False # 是否在图内
boss_pd = False # 是否到boss关卡
rw = (632, 378)
v = group['move_velocity'] # 从配置读取移动速度
def yolo_shibie(im_PIL, detections, model):
results = model(im_PIL)
for result in results:
for i in range(len(result.boxes.xyxy)):
left, top, right, bottom = result.boxes.xyxy[i]
scalar_tensor = result.boxes.cls[i]
value = scalar_tensor.item()
label = result.names[int(value)]
if label == 'center' or label == 'next' or label == 'boss' or label == 'zhaozi':
player_x = int(left + (right - left) / 2)
player_y = int(top + (bottom - top) / 2) + 30
RW = [player_x, player_y]
detections[label] = RW
elif label == 'daojv' or label == 'gw':
player_x = int(left + (right - left) / 2)
player_y = int(top + (bottom - top) / 2) + 30
RW = [player_x, player_y]
detections[label].append(RW)
elif label == 'npc1' or label == 'npc2' or label == 'npc3' or label == 'npc4':
player_x = int(left + (right - left) / 2)
player_y = int(bottom) + 30
RW = [player_x, player_y]
detections[label] = RW
return detections
def sq(p1, p2):
"""计算两点之间的欧式距离"""
return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
def process_points(points):
if not points:
return None
n = len(points)
if n == 1:
return points[0]
elif n == 2:
x = (points[0][0] + points[1][0]) / 2
y = (points[0][1] + points[1][1]) / 2
return [x, y]
else:
sample_points = random.sample(points, 3)
min_sum = float('inf')
best_point = None
for p in points:
dist_sum = sum(sq(p, sp) for sp in sample_points)
if dist_sum < min_sum:
min_sum = dist_sum
best_point = p
return best_point
def move_randomly(rw, k):
k = k % 4
suiji_t = float(random.randint(10, 13) / 10)
if k == 0:
keyboard.send_data("66")
time.sleep(suiji_t)
keyboard.release()
elif k == 1:
keyboard.send_data("88")
time.sleep(suiji_t)
keyboard.release()
elif k == 2:
keyboard.send_data("44")
time.sleep(suiji_t)
keyboard.release()
elif k == 3:
keyboard.send_data("22")
time.sleep(suiji_t)
keyboard.release()
return k + 1
def move_to(rw, mb):
"""使用配置的v值"""
nonlocal v
v = group['move_velocity']
if rw[0] >= mb[0]:
keyboard.send_data("44")
time.sleep(float(abs(rw[0] - mb[0]) / v))
keyboard.release()
else:
keyboard.send_data("66")
time.sleep(float(abs(rw[0] - mb[0]) / v))
keyboard.release()
if rw[1] >= mb[1]:
keyboard.send_data("88")
time.sleep(float(abs(rw[1] - mb[1]) / v))
keyboard.release()
else:
keyboard.send_data("22")
time.sleep(float(abs(rw[1] - mb[1]) / v))
keyboard.release()
# 主循环
print(f"🔄 配置组 {group['name']} 开始自动化循环...")
while True:
try:
detections = {
'center': None,
'next': None,
'npc1': None,
'npc2': None,
'npc3': None,
'npc4': None,
'boss': None,
'daojv': [],
'gw': [],
'zhaozi': None
}
im_opencv = get_image.get_frame()
if im_opencv is None:
time.sleep(0.1)
continue
detections = yolo_shibie(im_opencv[1], detections, model)
if shizi.tuwai(im_opencv[0]):
im_opencv = get_image.get_frame()
if im_opencv is None:
time.sleep(0.1)
continue
detections = yolo_shibie(im_opencv[1], detections, model0)
print(f'[{group["name"]}] 当前在城镇中')
if detections['npc1'] is not None and sq(rw, detections['npc1']) > 80:
print(f"[{group['name']}] 向npc1靠近")
move_to(rw, detections['npc1'])
continue
elif detections['npc1'] is not None and sq(rw, detections['npc1']) <= 80:
print(f"[{group['name']}] 在npc旁边向上走")
mb = (detections['npc1'][0], detections['npc1'][1] - 1010)
move_to(rw, mb)
continue
elif detections['npc3'] is not None and detections['npc4'] is None:
print(f"[{group['name']}] 在npc3旁边向右走")
mb = (rw[0], detections['npc3'][1] - 50)
move_to(rw, mb)
mb = (rw[0] + 700, rw[1])
move_to(rw, mb)
continue
elif detections['npc4'] is not None:
if sq(detections['npc4'], rw) < 50:
print(f"[{group['name']}] 离npc4很近 直接进入")
keyboard.send_data("DD")
time.sleep(0.15)
keyboard.release()
time.sleep(1)
im_opencv = get_image.get_frame()
if im_opencv and shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1)
continue
else:
print(f"[{group['name']}] 离npc4有点远 点击进入")
move_to(rw, detections['npc4'])
time.sleep(1)
im_opencv = get_image.get_frame()
if im_opencv and shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1)
continue
elif shizi.tiaozhan(im_opencv[0]):
print(f'[{group["name"]}] 进入塔4')
mouse_gui.send_data_absolute(left + 1100, top + 600, may=1)
time.sleep(0.3)
mouse_gui.send_data_absolute(left + 433, top + 455, may=1)
panduan = True
continue
elif shizi.jieshu(im_opencv[0]):
print(f'[{group["name"]}] 结束挑战')
mouse_gui.send_data_absolute(left + 542, top + 644, may=1)
time.sleep(0.8)
mouse_gui.send_data_absolute(left + 706, top + 454, may=1)
continue
elif panduan:
if shizi.shuzi(im_opencv[0]):
boss_pd = True
print(f"[{group['name']}] 进入到boss")
if shizi.fuhuo(im_opencv[0]):
print(f'[{group["name"]}] 点击复活')
mouse_gui.send_data_absolute(left + 536, top + 627, may=1)
mouse_gui.send_data_absolute(rw[0], rw[1], may=0)
continue
if detections['zhaozi'] is not None:
move_to(rw, detections['zhaozi'])
continue
if len(detections['daojv']) != 0:
move_to(rw, process_points(detections['daojv']))
for i in range(3 + len(detections['daojv'])):
keyboard.send_data("AA")
time.sleep(0.15)
keyboard.release()
continue
if shizi.tuichu(im_opencv[0]) and detections['next'] is None and len(detections['daojv']) == 0 and len(detections['gw']) == 0 and boss_pd:
print(f"[{group['name']}] 识别到可以退出挑战!!!!!!!!!!!!!!!!!!")
for i in range(3):
time.sleep(0.5)
im_opencv = get_image.get_frame()
if im_opencv is None:
continue
detections = {
'center': None, 'next': None,
'npc1': None, 'npc2': None, 'npc3': None, 'npc4': None,
'boss': None, 'zhaozi': None,
'daojv': [], 'gw': []
}
detections = yolo_shibie(im_opencv[1], detections, model)
if detections['next'] is not None or len(detections['daojv']) != 0 or len(detections['gw']) != 0 or detections['boss'] is not None:
break
else:
mouse_gui.send_data_absolute(left + 640, top + 40, may=1)
panduan = False
boss_pd = False
time.sleep(2.0)
continue
if detections['center'] is None and detections['next'] is None and (len(detections['gw']) != 0 or detections["boss"] is not None):
if len(detections['gw']) != 0:
move_to(rw, detections['gw'][0])
time.sleep(0.26)
elif detections['boss'] is not None:
boss_suiji1 = random.randint(-30, 30)
boss_suiji2 = random.randint(-30, 30)
detections['boss'] = (detections['boss'][0] + boss_suiji1, detections['boss'][1] + boss_suiji2)
move_to(rw, detections['boss'])
time.sleep(0.7)
continue
elif (detections['center'] is not None and detections['next'] is None and len(detections['gw']) != 0) or (boss_pd == True and detections['center'] is not None and detections['next'] is None):
if detections['center'][0] >= rw[0] and detections['center'][1] < rw[1]:
mb = (rw[0] + 100, rw[1])
elif detections['center'][0] <= rw[0] and detections['center'][1] < rw[1]:
mb = (rw[0], rw[1] - 100)
elif detections['center'][0] <= rw[0] and detections['center'][1] > rw[1]:
mb = (rw[0] - 100, rw[1])
elif detections['center'][0] >= rw[0] and detections['center'][1] > rw[1]:
mb = (rw[0], rw[1] + 100)
move_to(rw, mb)
time.sleep(0.25)
continue
elif boss_pd == True and detections['center'] is None and detections['next'] is None:
k = move_randomly(rw, k)
continue
elif detections['next'] is not None:
print(f'[{group["name"]}] 进入下一层啦啦啦啦啦啦')
panduan = True
move_to(rw, detections['next'])
for i in range(2):
keyboard.send_data("DD")
time.sleep(0.15)
keyboard.release()
continue
else:
k = move_randomly(rw, k)
continue
elif shizi.daoying(im_opencv[0]):
mouse_gui.send_data_absolute(rw[0], rw[1] - 110, may=1)
time.sleep(1)
continue
except KeyboardInterrupt:
print(f"🛑 配置组 {group['name']} 停止运行")
break
except Exception as e:
print(f"❌ 配置组 {group['name']} 运行错误: {e}")
import traceback
traceback.print_exc()
time.sleep(1)
if __name__ == "__main__":
if len(sys.argv) > 1:
group_index = int(sys.argv[1])
run_automation_for_group(group_index)
else:
# 默认运行活动配置组
active_group = config_manager.get_active_group()
if active_group is None:
print("❌ 没有活动的配置组")
sys.exit(1)
group_index = config_manager.config['groups'].index(active_group)
run_automation_for_group(group_index)

586
preview.py Normal file
View File

@@ -0,0 +1,586 @@
import cv2
import tkinter as tk
from tkinter import Canvas
from PIL import Image, ImageTk
import threading
import numpy as np
import warnings
import os
from config import config_manager
# 抑制OpenCV的警告信息兼容不同版本
import sys
import io
import time as _time
os.environ['OPENCV_LOG_LEVEL'] = 'SILENT'
os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '0'
try:
if hasattr(cv2, 'setLogLevel'):
if hasattr(cv2, 'LOG_LEVEL_SILENT'):
cv2.setLogLevel(cv2.LOG_LEVEL_SILENT)
elif hasattr(cv2, 'LOG_LEVEL_ERROR'):
cv2.setLogLevel(cv2.LOG_LEVEL_ERROR)
elif hasattr(cv2, 'utils'):
cv2.utils.setLogLevel(0)
except Exception:
pass
# 简单日志限流
_last_log_times = {}
def log_throttle(key: str, interval_sec: float, text: str):
now = _time.time()
last = _last_log_times.get(key, 0)
if now - last >= interval_sec:
_last_log_times[key] = now
try:
print(text)
except Exception:
pass
class PreviewWindow:
"""采集卡预览窗口"""
def __init__(self):
self.config = config_manager.config
self.caps = {}
self.frames = {}
self.large_windows = {} # idx -> Toplevel
self.running = True
def init_cameras(self):
"""初始化所有相机"""
print("🔧 开始初始化采集卡...")
loaded_count = 0
# 根据配置选择加载哪些组
display = self.config.get('display', {})
use_all = bool(display.get('preview_use_all_groups', True))
if use_all:
target_groups = self.config['groups']
print(f"📷 预览全部配置组,共 {len(target_groups)}")
else:
target_groups = [g for g in self.config['groups'] if g.get('active', False)]
print(f"📷 仅预览活动配置组,共 {len(target_groups)}")
if not target_groups:
print("⚠️ 没有活动的配置组,将不显示任何采集卡。可在配置中启用‘预览全部配置组’或设置活动组")
# 重定向stderr来抑制OpenCV的错误输出
old_stderr = sys.stderr
suppressed_output = io.StringIO()
try:
sys.stderr = suppressed_output
for i, group in enumerate(target_groups):
try:
cam_idx = group['camera_index']
print(f" 尝试打开采集卡 {cam_idx} ({group['name']})...")
cap = None
# 尝试多种后端打开
backends_to_try = [
(int(cam_idx), cv2.CAP_DSHOW),
(int(cam_idx), cv2.CAP_ANY),
(int(cam_idx), None),
]
for idx, backend in backends_to_try:
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore')
if backend is not None:
cap = cv2.VideoCapture(idx, backend)
else:
cap = cv2.VideoCapture(idx)
if cap.isOpened():
# 测试读取一帧
ret, test_frame = cap.read()
if ret and test_frame is not None:
break
else:
cap.release()
cap = None
except Exception:
if cap:
try:
cap.release()
except:
pass
cap = None
continue
if cap and cap.isOpened():
try:
cap.set(cv2.CAP_PROP_FRAME_WIDTH, group['camera_width'])
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, group['camera_height'])
except Exception as e:
print(f" ⚠️ 设置分辨率失败: {e}")
self.caps[i] = {
'cap': cap,
'group': group,
'name': group['name']
}
loaded_count += 1
print(f" ✅ 采集卡 {cam_idx} 初始化成功")
else:
print(f" ❌ 采集卡 {cam_idx} 无法打开")
except Exception as e:
print(f" ❌ 采集卡 {group.get('camera_index', '?')} 初始化失败: {e}")
import traceback
traceback.print_exc()
finally:
# 恢复stderr
sys.stderr = old_stderr
if loaded_count == 0:
print("⚠️ 警告:没有成功加载任何采集卡!")
print("请检查:")
print("1. 采集卡是否正确连接")
print("2. 采集卡索引是否正确")
print("3. 是否有活动的配置组")
else:
print(f"✅ 成功加载 {loaded_count} 个采集卡")
def grab_once(self):
"""抓取每個采集卡當前單幀(裁剪後,存入 self.frames抓取结果增强debug输出"""
for idx, data in self.caps.items():
try:
cap = data['cap']
frm_name = data.get('name', str(idx))
if cap is None:
print(f"[抓取] 采集卡{idx}({frm_name}) cap为None")
self.frames[idx] = None
continue
ret, frame = cap.read()
if not ret or frame is None:
print(f"[抓取] 采集卡{idx}({frm_name}) 没抓到帧 ret={ret} frame=None")
self.frames[idx] = None
continue
height, width = frame.shape[:2]
crop_top = 30
crop_bottom = min(crop_top + 720, height)
crop_width = min(1280, width)
if crop_bottom <= crop_top or crop_width <= 0:
print(f"[抓取] 采集卡{idx}({frm_name}) 尺寸异常: H={height} W={width} 裁剪失败")
self.frames[idx] = None
continue
frame = frame[crop_top:crop_bottom, 0:crop_width]
# 额外检查:像素全黑?
if np.mean(frame) < 3:
print(f"[抓取] 采集卡{idx}({frm_name}) 帧抓到但内容接近全黑 shape={frame.shape}")
else:
print(f"[抓取] 采集卡{idx}({frm_name}) 成功 shape={frame.shape} 类型={frame.dtype}")
self.frames[idx] = frame
except Exception as e:
print(f"[抓取] 采集卡{idx}({data.get('name', str(idx))}) 抓取异常: {e}")
self.frames[idx] = None
def capture_frames(self):
"""捕获帧"""
while self.running:
for idx, data in self.caps.items():
try:
ret, frame = data['cap'].read()
if ret and frame is not None:
# 裁剪到配置的区域
height, width = frame.shape[:2]
crop_top = 30
crop_bottom = min(crop_top + 720, height)
crop_width = min(1280, width)
# 确保裁剪范围有效
if crop_bottom > crop_top and crop_width > 0:
frame = frame[crop_top:crop_bottom, 0:crop_width]
# 转换颜色空间
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
self.frames[idx] = frame_rgb
else:
print(f"⚠️ 采集卡 {idx} 裁剪参数无效")
else:
# 读取失败,清除旧帧
if idx in self.frames:
self.frames[idx] = None
except Exception as e:
print(f"捕获帧 {idx} 错误: {e}")
import traceback
traceback.print_exc()
import time
time.sleep(0.01) # 避免CPU占用过高
def create_grid_window(self, master=None):
"""创建网格窗口可挂载在主Tk下作为Toplevel弹窗"""
display = self.config.get('display', {})
preview_width = display.get('preview_width', 1000)
preview_height = display.get('preview_height', 700)
columns = display.get('preview_columns', 2)
rows = display.get('preview_rows', 2)
if master is not None:
root = tk.Toplevel(master)
else:
root = tk.Tk()
root.title("采集卡预览 - 点击放大")
root.geometry(f"{preview_width}x{preview_height}")
root.update_idletasks()
canvas = Canvas(root, bg='black', width=preview_width, height=preview_height)
canvas.pack(fill=tk.BOTH, expand=True)
# 存储图像对象
self.photo_objects = {}
# 用于控制调试输出(只打印前几次)
self.debug_count = 0
def update_frames_once():
"""在主线程中每5秒刷新画面直接显示高频采集线程生成的帧去除grab_once"""
if not self.running:
return
try:
if not self.caps:
canvas.delete("all")
canvas.create_text(
root.winfo_width() // 2,
root.winfo_height() // 2,
text="未找到可用的采集卡\n\n请检查:\n1. 采集卡是否正确连接\n2. 配置组是否有活动的采集卡\n3. 采集卡索引是否正确",
fill='yellow',
font=('Arial', 14),
justify=tk.CENTER
)
root.after(5000, update_frames_once)
return
# 计算每个预览窗口的位置和大小
# 直接使用配置值作为画布尺寸macOS上窗口尺寸可能在显示前返回默认值
canvas_width = preview_width
canvas_height = preview_height
# 尝试获取实际的窗口尺寸,如果有效则使用(大于配置值说明可能被手动调整了)
try:
root.update_idletasks()
actual_width = root.winfo_width()
actual_height = root.winfo_height()
# 只有在获取到合理的尺寸时才使用大于100像素
if actual_width > 100 and actual_height > 100:
canvas_width = actual_width
canvas_height = actual_height
except:
pass # 如果获取失败,使用配置值
cell_width = max(10, canvas_width // columns)
cell_height = max(10, canvas_height // rows)
# 先收集所有需要显示的图像
images_to_draw = []
texts_to_draw = []
frame_idx = 0
for idx in self.caps.keys():
frame = self.frames.get(idx)
has_frame = frame is not None
print(f'[DEBUG] update UI idx={idx}, has_frame={has_frame}, frame_info={getattr(frame, "shape", None)}')
name = self.caps[idx]['name']
row = frame_idx // columns
col = frame_idx % columns
x = col * cell_width
y = row * cell_height
center_x = x + cell_width // 2
center_y = y + cell_height // 2
# 如果没拿到frame临时主动再抓一次并DEBUG
if not has_frame:
print(f'[DEBUG] UI未获得frame尝试临时抓取采集卡{idx}...')
try:
cap_test = self.caps[idx]['cap']
ret, fr2 = cap_test.read()
print(f'[DEBUG] 临时抓取ret={ret}, shape={getattr(fr2, "shape", None)}')
if ret and fr2 is not None:
frame = fr2
has_frame = True
else:
frame = None
has_frame = False
except Exception as e:
print(f'[DEBUG] 临时抓取异常:{e}')
frame = None
has_frame = False
if has_frame:
try:
h, w = frame.shape[:2]
if w <= 0 or h <= 0:
detail = f"采集卡{idx}:{name}\n抓到帧但图像尺寸异常 ({w}x{h})"
texts_to_draw.append((center_x, center_y, detail, 'red'))
frame_idx += 1
continue
scale = min(cell_width / w, cell_height / h, 1.0) * 0.85
new_w = max(1, int(w * scale))
new_h = max(1, int(h * scale))
if new_w <= 0 or new_h <= 0:
detail = f"采集卡{idx}:{name}\n缩放计算异常 ({w}x{h})"
texts_to_draw.append((center_x, center_y, detail, 'red'))
frame_idx += 1
continue
display_frame = frame
if len(display_frame.shape) == 3 and display_frame.shape[2] == 3:
display_frame = cv2.cvtColor(display_frame, cv2.COLOR_BGR2RGB)
resized_frame = cv2.resize(display_frame, (new_w, new_h))
pil_image = Image.fromarray(resized_frame)
photo = ImageTk.PhotoImage(image=pil_image)
if not hasattr(self, 'photo_refs'):
self.photo_refs = {}
self.photo_refs[idx] = photo
canvas.create_image(center_x, center_y, image=photo, anchor='center')
canvas.update() # 保证实时刷新
print(f'[DEBUG] UI idx={idx}, 绘制photo id={id(photo)}, photo_refs={len(self.photo_refs)}')
images_to_draw.append((photo, center_x, center_y))
texts_to_draw.append((center_x, y + 15, name, 'white'))
frame_idx += 1
except Exception as e:
errstr = str(e)
detail = f"采集卡{idx}:{name}\n帧处理异常:{errstr}"
print(f'[DEBUG] idx={idx}, 帧处理异常:{errstr}')
texts_to_draw.append((center_x, center_y, detail, 'red'))
frame_idx += 1
else:
detail = f"采集卡{idx}:{name}\n抓取失败/暂无画面"
print(f'[DEBUG] idx={idx} 依然无可用帧,最终显示错误提示')
texts_to_draw.append((center_x, center_y, detail, 'red'))
frame_idx += 1
# 清空画布
canvas.delete("all")
# 先绘制所有图像(底层)
if images_to_draw and self.debug_count <= 3:
log_throttle("draw_prepare", 2.0, f"✅ 准备绘制 {len(images_to_draw)} 个图像到画布 ({canvas_width}x{canvas_height})")
for photo, x, y in images_to_draw:
try:
# PhotoImage强引用防止被GC
if not hasattr(self, 'photo_refs'):
self.photo_refs = {}
self.photo_refs[idx] = photo
canvas.create_image(x, y, image=photo, anchor='center')
canvas.update() # 保证实时刷新
print(f'[DEBUG] idx={idx}, photo id={id(photo)}, photo refs={len(self.photo_refs)}')
except Exception as e:
if "pyimage" not in str(e).lower():
log_throttle("draw_image_error", 2.0, f"绘制图像错误: {e}")
# 再绘制所有文本(上层)
for x, y, text, color in texts_to_draw:
try:
if color == 'white':
canvas.create_text(x, y, text=text, fill=color, font=('Arial', 10, 'bold'))
else:
canvas.create_text(x, y, text=text, fill=color, font=('Arial', 10))
except Exception as e:
log_throttle("draw_text_error", 2.0, f"绘制文本错误: {e}")
# 绘制分割线,区分不同的采集卡窗口
try:
# 绘制垂直分割线(列之间的分割线)
for col in range(1, columns):
x = col * cell_width
canvas.create_line(
x, 0,
x, canvas_height,
fill='white',
width=2,
dash=(5, 5) # 虚线效果,让分割线更明显
)
# 绘制水平分割线(行之间的分割线)
for row in range(1, rows):
y = row * cell_height
canvas.create_line(
0, y,
canvas_width, y,
fill='white',
width=2,
dash=(5, 5) # 虚线效果
)
except Exception as e:
log_throttle("draw_grid_error", 2.0, f"绘制分割线错误: {e}")
except Exception as e:
log_throttle("update_frame_error", 1.0, f"更新帧错误: {e}")
import traceback
traceback.print_exc()
# 每5秒更新一次
root.after(5000, update_frames_once)
def on_canvas_click(event):
"""点击画布事件"""
window_width = root.winfo_width()
window_height = root.winfo_height()
cell_width = window_width // columns
cell_height = window_height // rows
col = int(event.x // cell_width)
row = int(event.y // cell_height)
index = row * columns + col
# 找到对应的配置
if index < len(self.caps):
idx = list(self.caps.keys())[index]
self.show_large_window(idx)
canvas.bind('<Button-1>', on_canvas_click)
# 等待窗口完全初始化后再开始更新
def start_updates():
"""延迟启动更新,确保窗口已完全显示"""
# 强制更新窗口尺寸
root.update_idletasks()
root.update()
# 等待窗口完全绘制
import time
time.sleep(0.2) # 给窗口更多时间初始化
root.update_idletasks()
root.update()
update_frames_once()
# 使用after在主线程中循环刷新延迟启动给足够时间让窗口初始化
root.after(200, start_updates)
def on_closing():
"""关闭窗口"""
self.running = False
for data in self.caps.values():
data['cap'].release()
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
# 仅在独立窗口无master时进入事件循环
if master is None:
root.mainloop()
def show_large_window(self, idx):
"""显示指定采集卡的大窗口(可多实例)"""
# 如果已存在该索引窗口,先销毁再创建,确保刷新
if idx in self.large_windows and self.large_windows[idx].winfo_exists():
try:
self.large_windows[idx].destroy()
except Exception:
pass
win = tk.Toplevel()
win.title(f"放大视图 - {self.caps[idx]['name']}")
win.geometry("1280x720")
self.large_windows[idx] = win
canvas = Canvas(win, bg='black')
canvas.pack(fill=tk.BOTH, expand=True)
photo_obj = {}
def update_large_once():
if not self.running or not win.winfo_exists():
return
try:
window_width = win.winfo_width() if win.winfo_width() > 1 else 1280
window_height = win.winfo_height() if win.winfo_height() > 1 else 720
if idx in self.frames and self.frames[idx] is not None:
frame = self.frames[idx]
h, w = frame.shape[:2]
scale = min(window_width / w, window_height / h)
new_w = int(w * scale)
new_h = int(h * scale)
resized_frame = cv2.resize(frame, (new_w, new_h))
if len(resized_frame.shape) == 3 and resized_frame.shape[2] == 3:
resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(resized_frame)
photo = ImageTk.PhotoImage(image=pil_image)
photo_obj['img'] = photo
canvas.delete("all")
canvas.create_image(window_width // 2, window_height // 2, image=photo, anchor='center')
else:
canvas.delete("all")
# 显示更详细的调试信息
debug_text = f"等待画面...\n采集卡: {idx}\n帧状态: {idx in self.frames}\n"
if idx in self.frames:
debug_text += f"帧数据: {self.frames[idx] is not None}"
else:
debug_text += "帧数据: 未初始化"
canvas.create_text(
window_width // 2,
window_height // 2,
text=debug_text,
fill='gray',
font=('Arial', 12),
justify='center'
)
except Exception as e:
if "pyimage" not in str(e).lower():
log_throttle("large_update_error", 1.0, f"更新大窗口错误: {e}")
# 放大視窗維持原本高頻刷新
win.after(33, update_large_once)
win.after(33, update_large_once)
def on_close():
try:
if idx in self.large_windows:
del self.large_windows[idx]
finally:
win.destroy()
win.protocol("WM_DELETE_WINDOW", on_close)
def run(self, master=None):
"""运行预览支持主Tk弹窗模式"""
self.init_cameras()
if not self.caps:
print("⚠️ 没有可用的采集卡,将显示错误提示窗口")
display = self.config.get('display', {})
multi_window = bool(display.get('preview_multi_window', False))
if self.caps:
capture_thread = threading.Thread(target=self.capture_frames, daemon=True)
capture_thread.start()
import time
time.sleep(0.5)
if multi_window:
root = tk.Toplevel(master) if master else tk.Tk()
root.title("采集卡预览 - 多窗口模式")
root.geometry("300x80")
tk.Label(root, text="已打开多窗口预览,可单独拖动/调整大小").pack(pady=20)
print("等待采集卡初始化...")
max_wait = 50
wait_count = 0
while wait_count < max_wait and not any(frame is not None for frame in self.frames.values()):
time.sleep(0.1)
wait_count += 1
if wait_count >= max_wait:
print("⚠️ 采集卡初始化超时,可能无法显示画面")
for idx in list(self.caps.keys()):
print(f"打开采集卡 {idx} 的预览窗口...")
self.show_large_window(idx)
def on_root_close():
self.running = False
for data in self.caps.values():
data['cap'].release()
try:
root.destroy()
except Exception:
pass
root.protocol("WM_DELETE_WINDOW", on_root_close)
# 仅在独立窗口无master时进入事件循环
if master is None:
root.mainloop()
else:
self.create_grid_window(master)
if __name__ == "__main__":
preview = PreviewWindow()
preview.run()

67
test_camera.py Normal file
View File

@@ -0,0 +1,67 @@
"""
采集卡测试脚本
用于测试采集卡是否能正常工作
"""
import cv2
from utils.get_image import GetImage
from config import config_manager
def test_camera():
"""测试采集卡"""
print("🔧 采集卡测试工具")
print("=" * 50)
# 获取活动配置
active_group = config_manager.get_active_group()
if active_group is None:
print("❌ 没有活动的配置组")
print("请先运行 python gui_config.py 设置配置")
return
print(f"📋 使用配置: {active_group['name']}")
print(f" 采集卡索引: {active_group['camera_index']}")
print(f" 分辨率: {active_group['camera_width']}x{active_group['camera_height']}")
print()
# 初始化采集卡
get_image = GetImage(
cam_index=active_group['camera_index'],
width=active_group['camera_width'],
height=active_group['camera_height']
)
if get_image.cap is None:
print("❌ 采集卡初始化失败")
return
print("✅ 采集卡初始化成功")
print("'q' 退出测试")
print()
# 测试循环
frame_count = 0
while True:
frame_data = get_image.get_frame()
if frame_data is None:
print("⚠️ 无法获取帧")
continue
frame_count += 1
if frame_count % 30 == 0: # 每30帧显示一次状态
print(f"📊 已获取 {frame_count}")
# 显示图像
cv2.imshow('采集卡测试', frame_data[0])
# 按 'q' 退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 清理
get_image.release()
cv2.destroyAllWindows()
print("🔚 测试结束")
if __name__ == "__main__":
test_camera()

796
test_capture_card.py Normal file
View File

@@ -0,0 +1,796 @@
"""
采集卡截图测试类
用于测试采集卡的分辨率和色差
保持与实际代码相同的实现逻辑
"""
import time
import threading
import warnings
import os
import sys
import io
import cv2
import numpy as np
from PIL import Image
from utils.logger import logger, throttle
import logging
# 抑制OpenCV的警告信息兼容不同版本
os.environ['OPENCV_LOG_LEVEL'] = 'SILENT'
os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '0'
try:
if hasattr(cv2, 'setLogLevel'):
if hasattr(cv2, 'LOG_LEVEL_SILENT'):
cv2.setLogLevel(cv2.LOG_LEVEL_SILENT)
elif hasattr(cv2, 'LOG_LEVEL_ERROR'):
cv2.setLogLevel(cv2.LOG_LEVEL_ERROR)
elif hasattr(cv2, 'utils'):
cv2.utils.setLogLevel(0)
except Exception:
pass
class CaptureCardTester:
"""
采集卡测试类
使用与实际代码相同的实现逻辑
"""
def __init__(self, cam_index=0, width=1920, height=1080):
"""
初始化采集卡测试器
:param cam_index: 采集卡索引
:param width: 期望宽度
:param height: 期望高度
"""
logger.info(f"🔧 正在初始化采集卡测试器 {cam_index}...")
self.cap = None
self.frame = None
self.running = True
self.cam_index = cam_index
self.expected_width = width
self.expected_height = height
self.actual_width = None
self.actual_height = None
# 尝试多种方式打开采集卡(与实际代码相同)
backends_to_try = [
(cam_index, cv2.CAP_DSHOW),
(cam_index, cv2.CAP_ANY),
(cam_index, None), # 默认后端
]
# 重定向stderr来抑制OpenCV的错误输出
old_stderr = sys.stderr
suppressed_output = io.StringIO()
try:
sys.stderr = suppressed_output
for idx, backend in backends_to_try:
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=UserWarning)
if backend is not None:
self.cap = cv2.VideoCapture(idx, backend)
else:
self.cap = cv2.VideoCapture(idx)
if self.cap.isOpened():
# 测试读取一帧
ret, test_frame = self.cap.read()
if ret and test_frame is not None:
logger.info(f"✅ 采集卡 {cam_index} 打开成功")
break
else:
self.cap.release()
self.cap = None
except Exception as e:
if self.cap:
try:
self.cap.release()
except:
pass
self.cap = None
continue
finally:
# 恢复stderr
sys.stderr = old_stderr
if self.cap is None or not self.cap.isOpened():
logger.error(f"❌ 无法打开采集卡 {cam_index}")
logger.error("请检查:\n 1. 采集卡是否正确连接\n 2. 采集卡索引是否正确(尝试扫描采集卡)\n 3. 采集卡驱动是否安装\n 4. 采集卡是否被其他程序占用")
self.cap = None
return
# 设置分辨率(与实际代码相同)
try:
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
# 实际获取设置后的分辨率
self.actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
logger.info(f" 分辨率设置: {width}x{height} -> 实际: {self.actual_width}x{self.actual_height}")
except Exception as e:
logger.warning(f"⚠️ 设置分辨率失败: {e}")
# 启动更新线程(与实际代码相同)
threading.Thread(target=self.update, daemon=True).start()
# 等待几帧确保采集卡正常工作
time.sleep(1.0)
logger.info(f"✅ 采集卡 {cam_index} 初始化完成")
def update(self):
"""持续更新帧(与实际代码相同)"""
while self.running and self.cap is not None:
try:
ret, frame = self.cap.read()
if ret and frame is not None:
self.frame = frame
# 限制读取频率避免占满CPU
time.sleep(0.008)
else:
# 读取失败时不打印,避免刷屏
time.sleep(0.02)
except Exception as e:
# 只在异常时打印错误
throttle(f"cap_read_err_{self.cam_index}", 2.0, logging.WARNING, f"⚠️ 采集卡 {self.cam_index} 读取异常: {e}")
time.sleep(0.1) # 出错时短暂延迟
def get_frame(self):
"""
获取处理后的帧(与实际代码相同)
返回: [im_opencv, im_PIL] 或 None
"""
if self.cap is None or self.frame is None:
return None
try:
im_opencv = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
im_opencv = im_opencv[30:30+720, 0:1280] # 裁剪尺寸(与实际代码相同)
im_PIL = Image.fromarray(im_opencv)
return [im_opencv, im_PIL]
except Exception as e:
throttle(f"img_proc_err_{self.cam_index}", 2.0, logging.WARNING, f"⚠️ 图像处理错误: {e}")
return None
def get_raw_frame(self):
"""
获取原始帧(未裁剪)
返回: numpy array 或 None
"""
if self.cap is None or self.frame is None:
return None
return self.frame.copy()
def test_resolution(self):
"""
测试分辨率
返回分辨率信息字典
"""
if self.cap is None:
logger.error("采集卡未初始化")
return None
result = {
'expected': (self.expected_width, self.expected_height),
'actual_cap': (self.actual_width, self.actual_height),
'actual_frame': None,
'cropped_frame': None,
'match': False
}
# 获取原始帧尺寸
raw_frame = self.get_raw_frame()
if raw_frame is not None:
h, w = raw_frame.shape[:2]
result['actual_frame'] = (w, h)
# 获取裁剪后帧尺寸
processed = self.get_frame()
if processed is not None:
im_opencv = processed[0]
h, w = im_opencv.shape[:2]
result['cropped_frame'] = (w, h)
# 检查分辨率是否匹配
if result['actual_cap'] is not None:
result['match'] = (result['actual_cap'][0] == self.expected_width and
result['actual_cap'][1] == self.expected_height)
return result
def test_color_difference(self, frame1=None, frame2=None):
"""
测试色差
:param frame1: 第一帧(可选,不提供则使用当前帧)
:param frame2: 第二帧(可选,不提供则等待一帧后获取)
:return: 色差信息字典
"""
if frame1 is None:
frame1 = self.get_frame()
if frame1 is None:
logger.error("无法获取第一帧")
return None
frame1 = frame1[0] # 使用opencv格式
if frame2 is None:
# 等待一小段时间获取新帧
time.sleep(0.1)
frame2 = self.get_frame()
if frame2 is None:
logger.error("无法获取第二帧")
return None
frame2 = frame2[0] # 使用opencv格式
# 确保两帧尺寸相同
if frame1.shape != frame2.shape:
logger.warning(f"两帧尺寸不同: {frame1.shape} vs {frame2.shape}")
# 调整尺寸
h, w = min(frame1.shape[0], frame2.shape[0]), min(frame1.shape[1], frame2.shape[1])
frame1 = frame1[:h, :w]
frame2 = frame2[:h, :w]
# 计算色差
diff = cv2.absdiff(frame1, frame2)
diff_gray = cv2.cvtColor(diff, cv2.COLOR_RGB2GRAY)
# 计算统计信息
mean_diff = np.mean(diff)
std_diff = np.std(diff)
max_diff = np.max(diff)
mean_diff_gray = np.mean(diff_gray)
# 计算RGB各通道的平均色差
mean_diff_r = np.mean(diff[:, :, 0])
mean_diff_g = np.mean(diff[:, :, 1])
mean_diff_b = np.mean(diff[:, :, 2])
# 计算PSNR峰值信噪比
mse = np.mean((frame1.astype(float) - frame2.astype(float)) ** 2)
if mse == 0:
psnr = float('inf')
else:
psnr = 20 * np.log10(255.0 / np.sqrt(mse))
result = {
'mean_diff': float(mean_diff),
'std_diff': float(std_diff),
'max_diff': int(max_diff),
'mean_diff_gray': float(mean_diff_gray),
'mean_diff_r': float(mean_diff_r),
'mean_diff_g': float(mean_diff_g),
'mean_diff_b': float(mean_diff_b),
'psnr': float(psnr),
'mse': float(mse),
'diff_image': diff,
'diff_gray': diff_gray
}
return result
def test_color_stability(self, num_frames=10, interval=0.1):
"""
测试颜色稳定性(连续多帧的色差)
:param num_frames: 测试帧数
:param interval: 帧间隔(秒)
:return: 稳定性统计信息
"""
frames = []
logger.info(f"开始采集 {num_frames} 帧用于稳定性测试...")
for i in range(num_frames):
frame = self.get_frame()
if frame is None:
logger.warning(f"无法获取第 {i+1}")
continue
frames.append(frame[0]) # 使用opencv格式
if i < num_frames - 1:
time.sleep(interval)
if len(frames) < 2:
logger.error("采集的帧数不足")
return None
# 计算所有帧之间的平均色差
all_diffs = []
for i in range(len(frames) - 1):
diff_result = self.test_color_difference(frames[i], frames[i+1])
if diff_result:
all_diffs.append(diff_result['mean_diff'])
if not all_diffs:
return None
result = {
'num_frames': len(frames),
'avg_mean_diff': float(np.mean(all_diffs)),
'std_mean_diff': float(np.std(all_diffs)),
'min_mean_diff': float(np.min(all_diffs)),
'max_mean_diff': float(np.max(all_diffs)),
'all_diffs': [float(d) for d in all_diffs]
}
return result
def print_resolution_test(self):
"""打印分辨率测试结果"""
print("\n" + "="*60)
print("分辨率测试结果")
print("="*60)
result = self.test_resolution()
if result is None:
print("❌ 测试失败:采集卡未初始化")
return
print(f"期望分辨率: {result['expected'][0]} x {result['expected'][1]}")
if result['actual_cap']:
print(f"实际分辨率(采集卡): {result['actual_cap'][0]} x {result['actual_cap'][1]}")
match_str = "✅ 匹配" if result['match'] else "❌ 不匹配"
print(f"分辨率匹配: {match_str}")
if result['actual_frame']:
print(f"实际分辨率(原始帧): {result['actual_frame'][0]} x {result['actual_frame'][1]}")
if result['cropped_frame']:
print(f"裁剪后分辨率: {result['cropped_frame'][0]} x {result['cropped_frame'][1]}")
print("="*60 + "\n")
def print_color_test(self):
"""打印色差测试结果"""
print("\n" + "="*60)
print("色差测试结果(两帧对比)")
print("="*60)
result = self.test_color_difference()
if result is None:
print("❌ 测试失败:无法获取帧")
return
print(f"平均色差: {result['mean_diff']:.2f}")
print(f"色差标准差: {result['std_diff']:.2f}")
print(f"最大色差: {result['max_diff']}")
print(f"灰度平均色差: {result['mean_diff_gray']:.2f}")
print(f"\nRGB通道平均色差:")
print(f" R通道: {result['mean_diff_r']:.2f}")
print(f" G通道: {result['mean_diff_g']:.2f}")
print(f" B通道: {result['mean_diff_b']:.2f}")
print(f"\nPSNR (峰值信噪比): {result['psnr']:.2f} dB")
print(f"MSE (均方误差): {result['mse']:.2f}")
print("="*60 + "\n")
def print_stability_test(self, num_frames=10):
"""打印稳定性测试结果"""
print("\n" + "="*60)
print(f"颜色稳定性测试结果({num_frames}帧)")
print("="*60)
result = self.test_color_stability(num_frames)
if result is None:
print("❌ 测试失败:无法获取足够的帧")
return
print(f"测试帧数: {result['num_frames']}")
print(f"平均色差均值: {result['avg_mean_diff']:.2f}")
print(f"色差标准差: {result['std_mean_diff']:.2f}")
print(f"最小色差: {result['min_mean_diff']:.2f}")
print(f"最大色差: {result['max_mean_diff']:.2f}")
print(f"\n各帧间色差: {[f'{d:.2f}' for d in result['all_diffs']]}")
print("="*60 + "\n")
def save_test_images(self, save_dir="test_output"):
"""保存测试图像"""
os.makedirs(save_dir, exist_ok=True)
# 保存原始帧
raw_frame = self.get_raw_frame()
if raw_frame is not None:
cv2.imwrite(os.path.join(save_dir, "raw_frame.jpg"), raw_frame)
logger.info(f"已保存原始帧: {save_dir}/raw_frame.jpg")
# 保存处理后的帧
processed = self.get_frame()
if processed is not None:
cv2.imwrite(os.path.join(save_dir, "processed_frame.jpg"), cv2.cvtColor(processed[0], cv2.COLOR_RGB2BGR))
logger.info(f"已保存处理后帧: {save_dir}/processed_frame.jpg")
# 保存色差图
color_diff = self.test_color_difference()
if color_diff is not None:
cv2.imwrite(os.path.join(save_dir, "color_diff.jpg"), cv2.cvtColor(color_diff['diff_image'], cv2.COLOR_RGB2BGR))
cv2.imwrite(os.path.join(save_dir, "color_diff_gray.jpg"), color_diff['diff_gray'])
logger.info(f"已保存色差图: {save_dir}/color_diff.jpg")
logger.info(f"已保存灰度色差图: {save_dir}/color_diff_gray.jpg")
def release(self):
"""释放资源(与实际代码相同)"""
self.running = False
time.sleep(0.2)
if self.cap is not None:
self.cap.release()
cv2.destroyAllWindows()
logger.info("🔚 采集卡已释放")
def __del__(self):
"""析构函数(与实际代码相同)"""
if hasattr(self, "cap") and self.cap is not None:
try:
self.release()
except:
pass
class MultiCaptureCardTester:
"""
多采集卡测试管理器
支持同时测试多张采集卡
"""
def __init__(self):
"""初始化多采集卡测试管理器"""
self.testers = {} # {cam_index: CaptureCardTester}
self.config = None
def load_from_config(self):
"""从配置文件加载采集卡"""
try:
from config import config_manager
config_manager.load_config()
self.config = config_manager.config
groups = self.config.get('groups', [])
if not groups:
logger.warning("配置文件中没有找到配置组")
return []
# 获取所有配置组中的采集卡
camera_configs = []
for group in groups:
cam_idx = group.get('camera_index')
cam_width = group.get('camera_width', 1920)
cam_height = group.get('camera_height', 1080)
name = group.get('name', f"配置组{groups.index(group)}")
# 检查是否已存在相同索引的采集卡
if cam_idx not in [c['index'] for c in camera_configs]:
camera_configs.append({
'index': cam_idx,
'width': cam_width,
'height': cam_height,
'name': name,
'group': group
})
return camera_configs
except Exception as e:
logger.error(f"从配置加载失败: {e}")
return []
def add_camera(self, cam_index, width=1920, height=1080, name=None):
"""
添加一张采集卡到测试列表
:param cam_index: 采集卡索引
:param width: 宽度
:param height: 高度
:param name: 采集卡名称(可选)
"""
if name is None:
name = f"采集卡{cam_index}"
if cam_index in self.testers:
logger.warning(f"采集卡 {cam_index} 已存在,将重新初始化")
self.testers[cam_index].release()
tester = CaptureCardTester(cam_index=cam_index, width=width, height=height)
if tester.cap is not None:
tester.name = name
self.testers[cam_index] = tester
return True
else:
logger.error(f"无法初始化采集卡 {cam_index}")
return False
def initialize_from_config(self, use_all_groups=True):
"""
从配置初始化所有采集卡
:param use_all_groups: 是否使用所有配置组False则只使用活动配置组
"""
camera_configs = self.load_from_config()
if not camera_configs:
logger.warning("没有找到可用的采集卡配置")
return False
# 筛选配置组
if not use_all_groups:
camera_configs = [c for c in camera_configs if c.get('group', {}).get('active', False)]
if not camera_configs:
logger.warning("没有活动的配置组")
return False
logger.info(f"📷 找到 {len(camera_configs)} 张采集卡配置")
success_count = 0
for config in camera_configs:
if self.add_camera(
cam_index=config['index'],
width=config['width'],
height=config['height'],
name=config['name']
):
success_count += 1
logger.info(f"✅ 成功初始化 {success_count}/{len(camera_configs)} 张采集卡")
return success_count > 0
def test_all_resolution(self):
"""测试所有采集卡的分辨率"""
print("\n" + "="*60)
print("所有采集卡分辨率测试")
print("="*60)
results = {}
for cam_index, tester in self.testers.items():
name = getattr(tester, 'name', f"采集卡{cam_index}")
print(f"\n{name} (索引: {cam_index})】")
result = tester.test_resolution()
results[cam_index] = result
# 汇总结果
print("\n" + "-"*60)
print("分辨率测试汇总")
print("-"*60)
for cam_index, result in results.items():
if result is None:
continue
name = getattr(self.testers[cam_index], 'name', f"采集卡{cam_index}")
match_str = "" if result.get('match', False) else ""
print(f"{match_str} {name}: 期望{result['expected']} -> 实际{result.get('actual_cap', 'N/A')}")
return results
def test_all_color(self):
"""测试所有采集卡的色差"""
print("\n" + "="*60)
print("所有采集卡色差测试")
print("="*60)
results = {}
for cam_index, tester in self.testers.items():
name = getattr(tester, 'name', f"采集卡{cam_index}")
print(f"\n{name} (索引: {cam_index})】")
result = tester.test_color_difference()
results[cam_index] = result
# 汇总结果
print("\n" + "-"*60)
print("色差测试汇总")
print("-"*60)
for cam_index, result in results.items():
if result is None:
continue
name = getattr(self.testers[cam_index], 'name', f"采集卡{cam_index}")
print(f"{name}: 平均色差={result['mean_diff']:.2f}, PSNR={result['psnr']:.2f}dB")
return results
def test_all_stability(self, num_frames=10):
"""测试所有采集卡的稳定性"""
print("\n" + "="*60)
print("所有采集卡稳定性测试")
print("="*60)
results = {}
for cam_index, tester in self.testers.items():
name = getattr(tester, 'name', f"采集卡{cam_index}")
print(f"\n{name} (索引: {cam_index})】")
result = tester.test_color_stability(num_frames=num_frames)
results[cam_index] = result
# 汇总结果
print("\n" + "-"*60)
print("稳定性测试汇总")
print("-"*60)
for cam_index, result in results.items():
if result is None:
continue
name = getattr(self.testers[cam_index], 'name', f"采集卡{cam_index}")
print(f"{name}: 平均色差={result['avg_mean_diff']:.2f}±{result['std_mean_diff']:.2f}")
return results
def save_all_test_images(self, base_dir="test_output"):
"""保存所有采集卡的测试图像"""
for cam_index, tester in self.testers.items():
name = getattr(tester, 'name', f"采集卡{cam_index}")
# 清理名称中的特殊字符,用于目录名
safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '-', '_')).rstrip()
save_dir = os.path.join(base_dir, safe_name)
tester.save_test_images(save_dir=save_dir)
def release_all(self):
"""释放所有采集卡"""
for tester in self.testers.values():
try:
tester.release()
except:
pass
self.testers.clear()
def __del__(self):
"""析构函数"""
self.release_all()
def scan_cameras(max_index=10):
"""
扫描可用的采集卡
:param max_index: 最大扫描索引
:return: 可用采集卡索引列表
"""
print("🔍 正在扫描采集卡...")
available = []
old_stderr = sys.stderr
suppressed_output = io.StringIO()
try:
sys.stderr = suppressed_output
for i in range(max_index):
cap = None
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore')
cap = cv2.VideoCapture(i, cv2.CAP_DSHOW)
if cap.isOpened():
ret, frame = cap.read()
if ret and frame is not None:
available.append(i)
print(f" ✅ 找到采集卡: 索引 {i}")
if cap:
cap.release()
except:
if cap:
try:
cap.release()
except:
pass
finally:
sys.stderr = old_stderr
if not available:
print(" ❌ 未找到可用的采集卡")
else:
print(f"✅ 共找到 {len(available)} 张采集卡")
return available
def main():
"""主测试函数"""
print("="*60)
print("采集卡截图测试工具(支持多采集卡)")
print("="*60)
multi_tester = MultiCaptureCardTester()
# 选择测试模式
print("\n选择测试模式:")
print(" 1. 从配置文件加载(所有配置组)")
print(" 2. 从配置文件加载(仅活动配置组)")
print(" 3. 手动指定采集卡索引")
print(" 4. 扫描采集卡")
print(" 0. 退出")
choice = input("\n请选择 (0-4): ").strip()
if choice == "0":
print("👋 退出")
return
elif choice == "1":
# 从配置加载所有配置组
if not multi_tester.initialize_from_config(use_all_groups=True):
print("❌ 无法从配置初始化采集卡")
return
elif choice == "2":
# 从配置加载活动配置组
if not multi_tester.initialize_from_config(use_all_groups=False):
print("❌ 无法从配置初始化采集卡")
return
elif choice == "3":
# 手动指定
indices_input = input("请输入采集卡索引(用逗号分隔,如: 0,1,2): ").strip()
try:
indices = [int(x.strip()) for x in indices_input.split(',')]
width_input = input("请输入宽度 (默认1920): ").strip()
height_input = input("请输入高度 (默认1080): ").strip()
width = int(width_input) if width_input else 1920
height = int(height_input) if height_input else 1080
success_count = 0
for idx in indices:
if multi_tester.add_camera(idx, width=width, height=height):
success_count += 1
if success_count == 0:
print("❌ 无法初始化任何采集卡")
return
except ValueError:
print("❌ 输入格式错误")
return
elif choice == "4":
# 扫描采集卡
available = scan_cameras()
if not available:
return
indices_input = input(f"请输入要测试的采集卡索引(用逗号分隔,可用: {available}): ").strip()
try:
indices = [int(x.strip()) for x in indices_input.split(',')]
# 验证索引有效性
indices = [i for i in indices if i in available]
if not indices:
print("❌ 没有有效的采集卡索引")
return
width_input = input("请输入宽度 (默认1920): ").strip()
height_input = input("请输入高度 (默认1080): ").strip()
width = int(width_input) if width_input else 1920
height = int(height_input) if height_input else 1080
success_count = 0
for idx in indices:
if multi_tester.add_camera(idx, width=width, height=height):
success_count += 1
if success_count == 0:
print("❌ 无法初始化任何采集卡")
return
except ValueError:
print("❌ 输入格式错误")
return
else:
print("❌ 无效选择")
return
if not multi_tester.testers:
print("❌ 没有可用的采集卡")
return
try:
# 等待采集卡稳定
print("\n等待采集卡稳定...")
time.sleep(1.0)
# 测试分辨率
multi_tester.test_all_resolution()
# 等待一下确保采集卡稳定
time.sleep(0.5)
# 测试色差
multi_tester.test_all_color()
# 测试稳定性
multi_tester.test_all_stability(num_frames=10)
# 保存测试图像
multi_tester.save_all_test_images()
print("\n✅ 所有测试完成!")
print("按 Enter 键退出...")
input()
except KeyboardInterrupt:
print("\n\n用户中断测试")
except Exception as e:
logger.error(f"测试过程中发生错误: {e}")
import traceback
traceback.print_exc()
finally:
multi_tester.release_all()
if __name__ == "__main__":
main()

View File

@@ -2,6 +2,8 @@ import time
from PIL import Image from PIL import Image
import cv2 import cv2
from utils.logger import logger, throttle
import logging
# class GetImage: # class GetImage:
# def __init__(self, cam_index=0, width=1920, height=1080): # def __init__(self, cam_index=0, width=1920, height=1080):
# self.cap = cv2.VideoCapture(cam_index,cv2.CAP_DSHOW) # self.cap = cv2.VideoCapture(cam_index,cv2.CAP_DSHOW)
@@ -41,33 +43,137 @@ import cv2
# cv2.destroyAllWindows() # cv2.destroyAllWindows()
import threading import threading
import warnings
# 抑制OpenCV的警告信息兼容不同版本
import os
import sys
import io
os.environ['OPENCV_LOG_LEVEL'] = 'SILENT'
os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '0'
try:
if hasattr(cv2, 'setLogLevel'):
if hasattr(cv2, 'LOG_LEVEL_SILENT'):
cv2.setLogLevel(cv2.LOG_LEVEL_SILENT)
elif hasattr(cv2, 'LOG_LEVEL_ERROR'):
cv2.setLogLevel(cv2.LOG_LEVEL_ERROR)
elif hasattr(cv2, 'utils'):
cv2.utils.setLogLevel(0)
except Exception:
pass
class GetImage: class GetImage:
def __init__(self, cam_index=0, width=1920, height=1080): def __init__(self, cam_index=0, width=1920, height=1080):
self.cap = cv2.VideoCapture(cam_index, cv2.CAP_DSHOW) logger.info(f"🔧 正在初始化采集卡 {cam_index}...")
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) self.cap = None
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
self.frame = None self.frame = None
self.running = True self.running = True
self.cam_index = cam_index
# 尝试多种方式打开采集卡
backends_to_try = [
(cam_index, cv2.CAP_DSHOW),
(cam_index, cv2.CAP_ANY),
(cam_index, None), # 默认后端
]
# 重定向stderr来抑制OpenCV的错误输出
old_stderr = sys.stderr
suppressed_output = io.StringIO()
try:
sys.stderr = suppressed_output
for idx, backend in backends_to_try:
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=UserWarning)
if backend is not None:
self.cap = cv2.VideoCapture(idx, backend)
else:
self.cap = cv2.VideoCapture(idx)
if self.cap.isOpened():
# 测试读取一帧
ret, test_frame = self.cap.read()
if ret and test_frame is not None:
logger.info(f"✅ 采集卡 {cam_index} 打开成功")
break
else:
self.cap.release()
self.cap = None
except Exception as e:
if self.cap:
try:
self.cap.release()
except:
pass
self.cap = None
continue
finally:
# 恢复stderr
sys.stderr = old_stderr
if self.cap is None or not self.cap.isOpened():
logger.error(f"❌ 无法打开采集卡 {cam_index}")
logger.error("请检查:\n 1. 采集卡是否正确连接\n 2. 采集卡索引是否正确(尝试扫描采集卡)\n 3. 采集卡驱动是否安装\n 4. 采集卡是否被其他程序占用")
self.cap = None
return
# 设置分辨率
try:
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
# 实际获取设置后的分辨率
actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
logger.info(f" 分辨率设置: {width}x{height} -> 实际: {actual_width}x{actual_height}")
except Exception as e:
logger.warning(f"⚠️ 设置分辨率失败: {e}")
# 启动更新线程
threading.Thread(target=self.update, daemon=True).start() threading.Thread(target=self.update, daemon=True).start()
# 等待几帧确保采集卡正常工作
import time
time.sleep(1.0)
logger.info(f"✅ 采集卡 {cam_index} 初始化完成")
def update(self): def update(self):
while self.running: while self.running and self.cap is not None:
ret, frame = self.cap.read() try:
if ret: ret, frame = self.cap.read()
self.frame = frame if ret and frame is not None:
self.frame = frame
# 限制读取频率避免占满CPU
time.sleep(0.008)
else:
# 读取失败时不打印,避免刷屏
time.sleep(0.02)
except Exception as e:
# 只在异常时打印错误
throttle(f"cap_read_err_{self.cam_index}", 2.0, logging.WARNING, f"⚠️ 采集卡 {self.cam_index} 读取异常: {e}")
time.sleep(0.1) # 出错时短暂延迟
def get_frame(self): def get_frame(self):
if self.frame is None: if self.cap is None or self.frame is None:
return None
try:
im_opencv = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
im_opencv = im_opencv[30:30+720, 0:1280]
im_PIL = Image.fromarray(im_opencv)
return [im_opencv, im_PIL]
except Exception as e:
throttle(f"img_proc_err_{self.cam_index}", 2.0, logging.WARNING, f"⚠️ 图像处理错误: {e}")
return None return None
im_opencv = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
im_opencv = im_opencv[30:30+720, 0:1280]
im_PIL = Image.fromarray(im_opencv)
return [im_opencv, im_PIL]
def release(self): def release(self):
self.running = False self.running = False
time.sleep(0.2) time.sleep(0.2)
self.cap.release() if self.cap is not None:
self.cap.release()
cv2.destroyAllWindows() cv2.destroyAllWindows()
get_image = GetImage()
# get_image 将在main.py中初始化
get_image = None

26
utils/logger.py Normal file
View File

@@ -0,0 +1,26 @@
import logging
import sys
import time
from typing import Dict
# 基础logger配置控制台输出
_logger = logging.getLogger("huojv")
if not _logger.handlers:
_logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
fmt = logging.Formatter(fmt='[%(asctime)s] %(levelname)s %(message)s', datefmt='%H:%M:%S')
handler.setFormatter(fmt)
_logger.addHandler(handler)
# 简单的节流打印同一个key在interval秒内只打印一次
_last_log_times: Dict[str, float] = {}
def throttle(key: str, interval_sec: float, level: int, msg: str):
now = time.time()
last = _last_log_times.get(key, 0.0)
if now - last >= interval_sec:
_last_log_times[key] = now
_logger.log(level, msg)
# 对外暴露
logger = _logger

View File

@@ -1,17 +1,34 @@
import random import random
import time import time
import ch9329Comm import ch9329Comm
import time
import random
import serial import serial
serial.ser = serial.Serial('COM6', 9600) # 开启串口
mouse = ch9329Comm.mouse.DataComm(1920, 1080) # 全局变量由main.py初始化
serial.ser = None
mouse = None
def init_mouse_keyboard(config_group):
"""初始化鼠标和串口"""
global serial, mouse
# 初始化串口
serial.ser = serial.Serial(
config_group['serial_port'],
config_group['serial_baudrate']
)
# 初始化鼠标
mouse = ch9329Comm.mouse.DataComm(
config_group['camera_width'],
config_group['camera_height']
)
print(f"✅ 串口已打开: {config_group['serial_port']} {config_group['serial_baudrate']}")
print(f"✅ 鼠标已初始化: {config_group['camera_width']}x{config_group['camera_height']}")
def bezier_point(t, p0, p1, p2, p3): def bezier_point(t, p0, p1, p2, p3):
"""计算三次贝塞尔曲线上的点""" """计算三次贝塞尔曲线上的点"""
x = (1-t)**3 * p0[0] + 3*(1-t)**2*t*p1[0] + 3*(1-t)*t**2*p2[0] + t**3*p3[0] x = (1-t)**3 * p0[0] + 3*(1-t)**2*t*p1[0] + 3*(1-t)*t**2*p2[0] + t**3*p3[0]
y = (1-t)**3 * p0[1] + 3*(1-t)**2*t*p1[1] + 3*(1-t)*t**2*p2[1] + t**3*p3[1] y = (1-t)**3 * p0[1] + 3*(1-t)**2*t*p1[1] + 3*(1-t)*t**2*p2[1] + t**3*p3[1]
return (x, y) return (x, y)
def move_mouse_bezier(mouse, start, end, duration=1, steps=120): def move_mouse_bezier(mouse, start, end, duration=1, steps=120):
""" """
用贝塞尔曲线模拟鼠标移动(安全版) 用贝塞尔曲线模拟鼠标移动(安全版)
@@ -45,13 +62,14 @@ class Mouse_guiji():
def __init__(self): def __init__(self):
self.point=(0,0) self.point=(0,0)
def send_data_absolute(self, x, y,may=0): def send_data_absolute(self, x, y, may=0):
move_mouse_bezier(mouse, self.point, (x,y), duration=1, steps=120) move_mouse_bezier(mouse, self.point, (x,y), duration=1, steps=120)
if may == 1:#点击左 if may == 1: # 点击左
mouse.click() mouse.click()
elif may == 2: elif may == 2:
mouse.click1()#点击右 mouse.click1() # 点击右
self.point=(x,y) self.point=(x,y)
mouse_gui = Mouse_guiji() # mouse_gui 将在main.py中初始化
mouse_gui = None

View File

@@ -45,7 +45,7 @@ def tiaozhan(image):
def tuichu(image): def tuichu(image):
image = image[24:58, 569:669] image = image[36:58, 560:669]
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
# 将裁剪后的图像编码成二进制格式 # 将裁剪后的图像编码成二进制格式
_, img_encoded = cv2.imencode('.png', image) _, img_encoded = cv2.imencode('.png', image)

View File

@@ -1,55 +1,397 @@
import cv2 import cv2
from utils.get_image import get_image from utils.get_image import GetImage
from ultralytics import YOLO from ultralytics import YOLO
from config import config_manager
from utils.logger import logger
import os
import numpy as np
model = YOLO(r"best0.pt").to('cuda') # 检查模型文件是否存在
model_path = r"best0.pt"
if not os.path.exists(model_path):
print(f"❌ 模型文件不存在: {model_path}")
exit(1)
def yolo_shibie(im_PIL, detections): # 加载YOLO模型
results = model(im_PIL) try:
result = results[0] model = YOLO(model_path).to('cuda')
print(f"✅ 模型加载成功: {model_path}")
except Exception as e:
print(f"❌ 模型加载失败: {e}")
exit(1)
# ✅ 获取绘制好框的图像 def enhance_sharpness(image, strength=1.5):
frame_with_boxes = result.plot() """
增强图像锐度
:param image: 输入图像BGR格式
:param strength: 锐化强度1.0-3.0默认1.5
:return: 锐化后的图像
"""
# 创建锐化核
kernel = np.array([[-1, -1, -1],
[-1, 9*strength, -1],
[-1, -1, -1]]) / (9*strength - 8)
sharpened = cv2.filter2D(image, -1, kernel)
return sharpened
# ✅ 用 OpenCV 动态显示
cv2.imshow("YOLO实时检测", frame_with_boxes)
# ESC 或 Q 键退出 def enhance_contrast(image, alpha=1.2, beta=10):
if cv2.waitKey(1) & 0xFF in [27, ord('q')]: """
return None 增强对比度和亮度
:param image: 输入图像
:param alpha: 对比度控制1.0-3.0默认1.2
:param beta: 亮度控制(-100到100默认10
:return: 增强后的图像
"""
return cv2.convertScaleAbs(image, alpha=alpha, beta=beta)
# ✅ 提取检测信息
for i in range(len(result.boxes.xyxy)):
left, top, right, bottom = result.boxes.xyxy[i]
cls_id = int(result.boxes.cls[i])
label = result.names[cls_id]
if label in ['center', 'next', 'npc1', 'npc2', 'npc3', 'npc4', 'boss', 'zhaozi']: def denoise_image(image, method='bilateral'):
player_x = int(left + (right - left) / 2) + 3 """
player_y = int(top + (bottom - top) / 2) + 40 去噪处理
detections[label] = [player_x, player_y] :param image: 输入图像
elif label in ['daojv', 'gw']: :param method: 去噪方法 ('bilateral', 'gaussian', 'fastNlMeans')
player_x = int(left + (right - left) / 2) + 3 :return: 去噪后的图像
player_y = int(top + (bottom - top) / 2) + 40 """
detections[label].append([player_x, player_y]) if method == 'bilateral':
# 双边滤波,保留边缘的同时去噪
return cv2.bilateralFilter(image, 9, 75, 75)
elif method == 'gaussian':
# 高斯模糊去噪
return cv2.GaussianBlur(image, (5, 5), 0)
elif method == 'fastNlMeans':
# 非局部均值去噪(效果最好但较慢)
return cv2.fastNlMeansDenoisingColored(image, None, 10, 10, 7, 21)
return image
def apply_enhancements(image, sharpness=True, contrast=True, denoise=True,
sharp_strength=1.5, contrast_alpha=1.2, contrast_beta=10,
denoise_method='bilateral'):
"""
应用所有图像增强
:param image: 输入图像BGR格式
:param sharpness: 是否锐化
:param contrast: 是否增强对比度
:param denoise: 是否去噪
:param sharp_strength: 锐化强度
:param contrast_alpha: 对比度系数
:param contrast_beta: 亮度调整
:param denoise_method: 去噪方法
:return: 增强后的图像
"""
enhanced = image.copy()
if denoise:
enhanced = denoise_image(enhanced, denoise_method)
if contrast:
enhanced = enhance_contrast(enhanced, contrast_alpha, contrast_beta)
if sharpness:
enhanced = enhance_sharpness(enhanced, sharp_strength)
return enhanced
def set_camera_properties(cap, brightness=None, contrast=None, saturation=None,
sharpness=None, gain=None, exposure=None):
"""
设置采集卡硬件参数
:param cap: VideoCapture对象
:param brightness: 亮度 (0-100)
:param contrast: 对比度 (0-100)
:param saturation: 饱和度 (0-100)
:param sharpness: 锐度 (0-100)
:param gain: 增益 (0-100)
:param exposure: 曝光 (通常为负值,如-6)
"""
props = {
cv2.CAP_PROP_BRIGHTNESS: brightness,
cv2.CAP_PROP_CONTRAST: contrast,
cv2.CAP_PROP_SATURATION: saturation,
cv2.CAP_PROP_SHARPNESS: sharpness,
cv2.CAP_PROP_GAIN: gain,
cv2.CAP_PROP_EXPOSURE: exposure,
}
for prop, value in props.items():
if value is not None:
try:
cap.set(prop, value)
actual = cap.get(prop)
logger.info(f" 设置 {prop.name if hasattr(prop, 'name') else prop}: {value} -> 实际: {actual:.2f}")
except Exception as e:
logger.warning(f" ⚠️ 设置参数 {prop} 失败: {e}")
def yolo_shibie(im_PIL, detections, model, enhance_enabled=False, enhance_params=None):
"""
YOLO识别函数
:param im_PIL: PIL图像对象
:param detections: 检测结果字典
:param model: YOLO模型
:param enhance_enabled: 是否启用图像增强
:param enhance_params: 图像增强参数
:return: 更新后的detections字典如果用户退出则返回None
"""
if im_PIL is None:
return detections
try:
results = model(im_PIL)
result = results[0]
# ✅ 获取绘制好框的图像RGB格式
frame_with_boxes_rgb = result.plot()
# ✅ 转换为BGR格式用于OpenCV显示
frame_with_boxes_bgr = cv2.cvtColor(frame_with_boxes_rgb, cv2.COLOR_RGB2BGR)
# 应用图像增强(如果启用)
display_frame = frame_with_boxes_bgr.copy()
if enhance_enabled and enhance_params:
try:
display_frame = apply_enhancements(display_frame, **enhance_params)
except Exception as e:
print(f"⚠️ 图像增强失败: {e}")
# 显示YOLO检测结果
cv2.imshow("YOLO Real-time Detection", display_frame)
# ✅ 提取检测信息
if result.boxes is not None and len(result.boxes.xyxy) > 0:
# 用于存储多个候选npc4如果检测到多个
npc4_candidates = []
for i in range(len(result.boxes.xyxy)):
try:
left = float(result.boxes.xyxy[i][0])
top = float(result.boxes.xyxy[i][1])
right = float(result.boxes.xyxy[i][2])
bottom = float(result.boxes.xyxy[i][3])
cls_id = int(result.boxes.cls[i])
label = result.names[cls_id]
# 获取置信度(如果可用)
confidence = float(result.boxes.conf[i]) if hasattr(result.boxes, 'conf') and len(result.boxes.conf) > i else 1.0
# npc1-npc4 使用底部位置与main.py保持一致
if label in ['npc1', 'npc2', 'npc3', 'npc4']:
player_x = int(left + (right - left) / 2)
player_y = int(bottom) + 30 # 使用底部位置与main.py保持一致
position = [player_x, player_y]
# 特殊处理npc4如果检测到多个收集所有候选
if label == 'npc4':
npc4_candidates.append({
'position': position,
'confidence': confidence,
'box': [left, top, right, bottom],
'area': (right - left) * (bottom - top) # 检测框面积
})
else:
# npc1-npc3直接赋值如果已经有值保留置信度更高的
if detections[label] is None or (hasattr(result.boxes, 'conf') and
confidence > 0.5):
detections[label] = position
# 其他目标使用中心点
elif label in ['center', 'next', 'boss', 'zhaozi']:
player_x = int(left + (right - left) / 2) + 3
player_y = int(top + (bottom - top) / 2) + 40
detections[label] = [player_x, player_y]
# 道具和怪物可以多个
elif label in ['daojv', 'gw']:
player_x = int(left + (right - left) / 2) + 3
player_y = int(top + (bottom - top) / 2) + 40
# 确保列表存在
if label not in detections:
detections[label] = []
detections[label].append([player_x, player_y])
except Exception as e:
print(f"⚠️ 处理检测框时出错: {e}")
continue
# 处理npc4如果检测到多个选择最合适的
if npc4_candidates:
# 按置信度排序,选择置信度最高的
npc4_candidates.sort(key=lambda x: x['confidence'], reverse=True)
# 选择最佳候选(置信度最高且面积合理)
best_npc4 = None
for candidate in npc4_candidates:
# 置信度阈值至少0.3(可根据实际情况调整)
if candidate['confidence'] >= 0.3:
# 检查检测框面积是否合理(避免过小的误检)
area = candidate['area']
if area > 100: # 最小面积阈值
best_npc4 = candidate
break
if best_npc4:
detections['npc4'] = best_npc4['position']
# 可选:输出调试信息
# print(f"✅ 检测到npc4: 位置={best_npc4['position']}, 置信度={best_npc4['confidence']:.2f}")
elif len(npc4_candidates) == 1:
# 如果只有一个候选,即使置信度较低也使用
detections['npc4'] = npc4_candidates[0]['position']
except Exception as e:
print(f"⚠️ YOLO检测出错: {e}")
return detections return detections
while True: def main():
detections = { """主函数"""
'center': None, 'next': None, print("="*60)
'npc1': None, 'npc2': None, 'npc3': None, 'npc4': None, print("YOLO实时检测测试")
'boss': None, 'zhaozi': None, print("="*60)
'daojv': [], 'gw': []
}
im_opencv = get_image.get_frame() # [RGB, PIL] # 从配置加载采集卡设置
detections = yolo_shibie(im_opencv[1], detections) active_group = config_manager.get_active_group()
if detections is None: # 用户退出 if active_group is None:
break print("⚠️ 没有活动的配置组,使用默认设置")
print("提示: 可以运行 python gui_config.py 设置配置")
cam_index = 0
width = 1920
height = 1080
else:
print(f"📋 使用配置组: {active_group['name']}")
cam_index = active_group['camera_index']
width = active_group['camera_width']
height = active_group['camera_height']
print(detections) print(f" 采集卡索引: {cam_index}")
print(f" 分辨率: {width}x{height}")
print()
cv2.destroyAllWindows() # 初始化采集卡
print("🔧 正在初始化采集卡...")
get_image = GetImage(
cam_index=cam_index,
width=width,
height=height
)
if get_image.cap is None:
print("❌ 采集卡初始化失败")
print("请检查:")
print("1. 采集卡是否正确连接")
print("2. 采集卡索引是否正确")
print("3. 采集卡驱动是否安装")
return
# 设置采集卡硬件参数以提高清晰度(可选)
print("\n🔧 设置采集卡参数以提高清晰度...")
print("提示: 可以根据实际情况调整这些参数")
set_camera_properties(
get_image.cap,
brightness=50, # 亮度 (0-100)
contrast=50, # 对比度 (0-100)
saturation=55, # 饱和度 (0-100)
sharpness=60, # 锐度 (0-100提高清晰度)
gain=None, # 增益 (根据实际情况调整)
exposure=None # 曝光 (根据实际情况调整,通常为负值)
)
print("✅ 采集卡初始化成功")
print("\n快捷键:")
print(" 'q' 或 ESC - 退出")
print(" 'e' - 切换图像增强")
print(" '1'/'2' - 调整锐化强度 (+/-0.1)")
print(" '3'/'4' - 调整对比度 (+/-0.1)")
print()
try:
frame_count = 0
enhance_enabled = False # 默认关闭图像增强
# 图像增强参数
enhance_params = {
'sharpness': True,
'contrast': True,
'denoise': True,
'sharp_strength': 1.5,
'contrast_alpha': 1.2,
'contrast_beta': 10,
'denoise_method': 'bilateral'
}
while True:
# 获取帧
frame_data = get_image.get_frame()
if frame_data is None:
print("⚠️ 无法获取帧,跳过...")
continue
# frame_data 是 [im_opencv_rgb, im_PIL] 格式
# im_opencv_rgb 已经是RGB格式经过BGR2RGB转换
im_opencv_rgb, im_PIL = frame_data
if im_PIL is None:
print("⚠️ PIL图像为空跳过...")
continue
# 初始化检测结果字典
detections = {
'center': None, 'next': None,
'npc1': None, 'npc2': None, 'npc3': None, 'npc4': None,
'boss': None, 'zhaozi': None,
'daojv': [], 'gw': []
}
# 执行YOLO检测
detections = yolo_shibie(im_PIL, detections, model, enhance_enabled, enhance_params)
# 检查按键
key = cv2.waitKey(1) & 0xFF
if key in [27, ord('q'), ord('Q')]:
print("\n用户退出")
break
elif key == ord('e') or key == ord('E'):
enhance_enabled = not enhance_enabled
status = "开启" if enhance_enabled else "关闭"
print(f"图像增强: {status} (锐化={enhance_params['sharp_strength']:.1f}, "
f"对比度={enhance_params['contrast_alpha']:.1f})")
elif key == ord('1'):
enhance_params['sharp_strength'] = min(3.0, enhance_params['sharp_strength'] + 0.1)
print(f"锐化强度: {enhance_params['sharp_strength']:.1f}")
elif key == ord('2'):
enhance_params['sharp_strength'] = max(0.5, enhance_params['sharp_strength'] - 0.1)
print(f"锐化强度: {enhance_params['sharp_strength']:.1f}")
elif key == ord('3'):
enhance_params['contrast_alpha'] = min(3.0, enhance_params['contrast_alpha'] + 0.1)
print(f"对比度: {enhance_params['contrast_alpha']:.1f}")
elif key == ord('4'):
enhance_params['contrast_alpha'] = max(0.5, enhance_params['contrast_alpha'] - 0.1)
print(f"对比度: {enhance_params['contrast_alpha']:.1f}")
frame_count += 1
if frame_count % 30 == 0: # 每30帧打印一次
print(f"📊 已处理 {frame_count}")
# 打印有检测到的目标
detected_items = {k: v for k, v in detections.items() if v is not None and v != []}
if detected_items:
print(f" 检测到: {detected_items}")
except KeyboardInterrupt:
print("\n\n用户中断测试")
except Exception as e:
print(f"\n❌ 测试过程中发生错误: {e}")
import traceback
traceback.print_exc()
finally:
# 清理资源
get_image.release()
cv2.destroyAllWindows()
print("🔚 测试结束")
if __name__ == "__main__":
main()

226
yolotest2.py Normal file
View File

@@ -0,0 +1,226 @@
"""
从main.py提取的YOLO识别测试文件
使用与main.py相同的识别逻辑
"""
import cv2
from utils.get_image import GetImage
from ultralytics import YOLO
from config import config_manager
import os
# 检查模型文件是否存在
model_path = r"best.pt"
model0_path = r"best0.pt"
if not os.path.exists(model_path):
print(f"❌ 模型文件不存在: {model_path}")
exit(1)
if not os.path.exists(model0_path):
print(f"❌ 模型文件不存在: {model0_path}")
exit(1)
# 加载YOLO模型与main.py保持一致
try:
model = YOLO(model_path).to('cuda')
model0 = YOLO(model0_path).to('cuda')
print(f"✅ 模型加载成功: {model_path}")
print(f"✅ 模型加载成功: {model0_path}")
except Exception as e:
print(f"❌ 模型加载失败: {e}")
exit(1)
def yolo_shibie(im_PIL, detections, model):
"""
YOLO识别函数与main.py中的实现完全一致
:param im_PIL: PIL图像对象
:param detections: 检测结果字典
:param model: YOLO模型
:return: 更新后的detections字典
"""
results = model(im_PIL) # 目标检测
for result in results:
for i in range(len(result.boxes.xyxy)):
left, top, right, bottom = result.boxes.xyxy[i]
scalar_tensor = result.boxes.cls[i]
value = scalar_tensor.item()
label = result.names[int(value)]
if label == 'center' or label == 'next' or label == 'boss' or label == 'zhaozi':
player_x = int(left + (right - left) / 2)
player_y = int(top + (bottom - top) / 2) + 30
RW = [player_x, player_y]
detections[label] = RW
elif label == 'daojv' or label == 'gw':
player_x = int(left + (right - left) / 2)
player_y = int(top + (bottom - top) / 2) + 30
RW = [player_x, player_y]
detections[label].append(RW)
elif label == 'npc1' or label == 'npc2' or label == 'npc3' or label == 'npc4':
player_x = int(left + (right - left) / 2)
player_y = int(bottom) + 30
RW = [player_x, player_y]
detections[label] = RW
return detections
def main():
"""主函数"""
print("="*60)
print("YOLO识别测试main.py逻辑")
print("="*60)
# 从配置加载采集卡设置
active_group = config_manager.get_active_group()
if active_group is None:
print("⚠️ 没有活动的配置组,使用默认设置")
print("提示: 可以运行 python gui_config.py 设置配置")
cam_index = 0
width = 1920
height = 1080
use_model = model # 默认使用model
else:
print(f"📋 使用配置组: {active_group['name']}")
cam_index = active_group['camera_index']
width = active_group['camera_width']
height = active_group['camera_height']
use_model = model0 # 城镇中使用model0
print(f" 使用模型: model0 (best0.pt) - 用于城镇识别")
print(f" 采集卡索引: {cam_index}")
print(f" 分辨率: {width}x{height}")
print()
# 初始化采集卡
print("🔧 正在初始化采集卡...")
get_image = GetImage(
cam_index=cam_index,
width=width,
height=height
)
if get_image.cap is None:
print("❌ 采集卡初始化失败")
print("请检查:")
print("1. 采集卡是否正确连接")
print("2. 采集卡索引是否正确")
print("3. 采集卡驱动是否安装")
return
print("✅ 采集卡初始化成功")
print("\n快捷键:")
print(" 'q' 或 ESC - 退出")
print(" 'm' - 切换模型 (model/model0)")
print(" 'd' - 显示/隐藏检测信息")
print()
try:
frame_count = 0
show_detections = True # 是否显示检测信息
current_model = use_model # 当前使用的模型
current_model_name = "model0" if use_model == model0 else "model"
while True:
# 获取帧
frame_data = get_image.get_frame()
if frame_data is None:
print("⚠️ 无法获取帧,跳过...")
continue
# frame_data 是 [im_opencv_rgb, im_PIL] 格式
im_opencv_rgb, im_PIL = frame_data
if im_PIL is None:
print("⚠️ PIL图像为空跳过...")
continue
# 初始化检测结果字典
detections = {
'center': None, 'next': None,
'npc1': None, 'npc2': None, 'npc3': None, 'npc4': None,
'boss': None, 'zhaozi': None,
'daojv': [], 'gw': []
}
# 执行YOLO检测使用main.py的逻辑
detections = yolo_shibie(im_PIL, detections, current_model)
# 获取绘制好框的图像用于显示
try:
results = current_model(im_PIL)
result = results[0]
frame_with_boxes_rgb = result.plot()
frame_with_boxes_bgr = cv2.cvtColor(frame_with_boxes_rgb, cv2.COLOR_RGB2BGR)
except Exception as e:
print(f"⚠️ 绘制检测框失败: {e}")
frame_with_boxes_bgr = cv2.cvtColor(im_opencv_rgb, cv2.COLOR_RGB2BGR)
# 在图像上显示检测信息
if show_detections:
# 显示模型名称
cv2.putText(frame_with_boxes_bgr, f"Model: {current_model_name}",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
# 显示检测到的目标
y_offset = 60
detected_items = []
for key, value in detections.items():
if value is not None and value != []:
if key in ['daojv', 'gw']:
detected_items.append(f"{key}: {len(value)}")
else:
detected_items.append(f"{key}: {value}")
if detected_items:
text = f"Detected: {', '.join(detected_items[:5])}" # 最多显示5个
if len(detected_items) > 5:
text += f" ... (+{len(detected_items)-5})"
cv2.putText(frame_with_boxes_bgr, text,
(10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2)
# 显示图像
cv2.imshow("YOLO Detection (main.py logic)", frame_with_boxes_bgr)
# 检查按键
key = cv2.waitKey(1) & 0xFF
if key in [27, ord('q'), ord('Q')]:
print("\n用户退出")
break
elif key == ord('m') or key == ord('M'):
# 切换模型
if current_model == model:
current_model = model0
current_model_name = "model0"
else:
current_model = model
current_model_name = "model"
print(f"切换模型: {current_model_name}")
elif key == ord('d') or key == ord('D'):
show_detections = not show_detections
print(f"显示检测信息: {'开启' if show_detections else '关闭'}")
frame_count += 1
if frame_count % 30 == 0: # 每30帧打印一次
print(f"📊 已处理 {frame_count} 帧 (模型: {current_model_name})")
# 打印有检测到的目标
detected_items = {k: v for k, v in detections.items() if v is not None and v != []}
if detected_items:
print(f" 检测到: {detected_items}")
except KeyboardInterrupt:
print("\n\n用户中断测试")
except Exception as e:
print(f"\n❌ 测试过程中发生错误: {e}")
import traceback
traceback.print_exc()
finally:
# 清理资源
get_image.release()
cv2.destroyAllWindows()
print("🔚 测试结束")
if __name__ == "__main__":
main()