roblox-studio-physical-operation-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- roblox_studio_physical_operation_mcp/__init__.py +13 -0
- roblox_studio_physical_operation_mcp/__main__.py +8 -0
- roblox_studio_physical_operation_mcp/log_filter.py +99 -0
- roblox_studio_physical_operation_mcp/log_utils.py +467 -0
- roblox_studio_physical_operation_mcp/server.py +602 -0
- roblox_studio_physical_operation_mcp/studio_manager.py +476 -0
- roblox_studio_physical_operation_mcp/toolbar_detector.py +513 -0
- roblox_studio_physical_operation_mcp/windows_utils.py +578 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/METADATA +273 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/RECORD +13 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/WHEEL +4 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- roblox_studio_physical_operation_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""
|
|
2
|
+
工具栏按钮状态检测模块 - 模板匹配版本
|
|
3
|
+
|
|
4
|
+
使用 OpenCV 模板匹配在截图中定位按钮,然后通过颜色判断状态。
|
|
5
|
+
|
|
6
|
+
按钮布局(从左到右):
|
|
7
|
+
1. 播放按钮(Play): 三角形 ▶
|
|
8
|
+
2. 暂停按钮(Pause): 双竖线 ‖
|
|
9
|
+
3. 停止按钮(Stop): 方块 ■
|
|
10
|
+
|
|
11
|
+
颜色状态:
|
|
12
|
+
- 灰色 = disabled
|
|
13
|
+
- 彩色 = enabled (绿色播放、蓝色暂停、红色停止)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import ctypes
|
|
17
|
+
import os
|
|
18
|
+
from typing import Optional, List, Tuple
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from enum import Enum
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import cv2
|
|
24
|
+
import numpy as np
|
|
25
|
+
import win32gui
|
|
26
|
+
import win32ui
|
|
27
|
+
from PIL import Image
|
|
28
|
+
except ImportError as e:
|
|
29
|
+
raise ImportError(f"需要安装依赖: pip install opencv-python pywin32 Pillow numpy: {e}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ButtonState(Enum):
|
|
33
|
+
"""按钮状态"""
|
|
34
|
+
DISABLED = "disabled" # 灰色,不可用
|
|
35
|
+
ENABLED = "enabled" # 彩色,可用
|
|
36
|
+
UNKNOWN = "unknown" # 无法识别
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ButtonType(Enum):
|
|
40
|
+
"""按钮类型"""
|
|
41
|
+
PLAY = "play"
|
|
42
|
+
PAUSE = "pause"
|
|
43
|
+
STOP = "stop"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ButtonInfo:
|
|
48
|
+
"""按钮信息"""
|
|
49
|
+
button_type: ButtonType
|
|
50
|
+
x: int
|
|
51
|
+
y: int
|
|
52
|
+
width: int
|
|
53
|
+
height: int
|
|
54
|
+
state: ButtonState
|
|
55
|
+
color_type: str # "gray", "green", "blue", "red"
|
|
56
|
+
confidence: float # 模板匹配置信度
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ToolbarState:
|
|
61
|
+
"""工具栏状态"""
|
|
62
|
+
play: ButtonState
|
|
63
|
+
pause: ButtonState
|
|
64
|
+
stop: ButtonState
|
|
65
|
+
game_state: str # "stopped", "running", "paused"
|
|
66
|
+
buttons: List[ButtonInfo] = None
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> dict:
|
|
69
|
+
result = {
|
|
70
|
+
"play": self.play.value,
|
|
71
|
+
"pause": self.pause.value,
|
|
72
|
+
"stop": self.stop.value,
|
|
73
|
+
"game_state": self.game_state
|
|
74
|
+
}
|
|
75
|
+
if self.buttons:
|
|
76
|
+
result["buttons_detail"] = [
|
|
77
|
+
{
|
|
78
|
+
"type": b.button_type.value,
|
|
79
|
+
"x": b.x, "y": b.y,
|
|
80
|
+
"width": b.width, "height": b.height,
|
|
81
|
+
"state": b.state.value,
|
|
82
|
+
"color_type": b.color_type,
|
|
83
|
+
"confidence": round(b.confidence, 3)
|
|
84
|
+
}
|
|
85
|
+
for b in self.buttons
|
|
86
|
+
]
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# 模板文件路径
|
|
91
|
+
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def capture_window_to_image(hwnd: int) -> Optional[Image.Image]:
|
|
95
|
+
"""截取窗口为 PIL Image 对象"""
|
|
96
|
+
try:
|
|
97
|
+
rect = win32gui.GetWindowRect(hwnd)
|
|
98
|
+
width = rect[2] - rect[0]
|
|
99
|
+
height = rect[3] - rect[1]
|
|
100
|
+
|
|
101
|
+
if width <= 0 or height <= 0:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
hwnd_dc = win32gui.GetWindowDC(hwnd)
|
|
105
|
+
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
|
|
106
|
+
save_dc = mfc_dc.CreateCompatibleDC()
|
|
107
|
+
|
|
108
|
+
bitmap = win32ui.CreateBitmap()
|
|
109
|
+
bitmap.CreateCompatibleBitmap(mfc_dc, width, height)
|
|
110
|
+
save_dc.SelectObject(bitmap)
|
|
111
|
+
|
|
112
|
+
ctypes.windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), 2)
|
|
113
|
+
|
|
114
|
+
bmpinfo = bitmap.GetInfo()
|
|
115
|
+
bmpstr = bitmap.GetBitmapBits(True)
|
|
116
|
+
img = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
|
|
117
|
+
bmpstr, 'raw', 'BGRX', 0, 1)
|
|
118
|
+
|
|
119
|
+
win32gui.DeleteObject(bitmap.GetHandle())
|
|
120
|
+
save_dc.DeleteDC()
|
|
121
|
+
mfc_dc.DeleteDC()
|
|
122
|
+
win32gui.ReleaseDC(hwnd, hwnd_dc)
|
|
123
|
+
|
|
124
|
+
return img
|
|
125
|
+
except Exception:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def pil_to_cv2(pil_image: Image.Image) -> np.ndarray:
|
|
130
|
+
"""PIL Image 转 OpenCV 格式"""
|
|
131
|
+
return cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def rgb_to_hsv(r: int, g: int, b: int) -> Tuple[float, float, float]:
|
|
135
|
+
"""RGB 转 HSV (H: 0-360, S: 0-100, V: 0-100)"""
|
|
136
|
+
r, g, b = r / 255.0, g / 255.0, b / 255.0
|
|
137
|
+
max_val = max(r, g, b)
|
|
138
|
+
min_val = min(r, g, b)
|
|
139
|
+
diff = max_val - min_val
|
|
140
|
+
|
|
141
|
+
v = max_val * 100
|
|
142
|
+
|
|
143
|
+
if max_val == 0:
|
|
144
|
+
s = 0
|
|
145
|
+
else:
|
|
146
|
+
s = (diff / max_val) * 100
|
|
147
|
+
|
|
148
|
+
if diff == 0:
|
|
149
|
+
h = 0
|
|
150
|
+
elif max_val == r:
|
|
151
|
+
h = 60 * (((g - b) / diff) % 6)
|
|
152
|
+
elif max_val == g:
|
|
153
|
+
h = 60 * (((b - r) / diff) + 2)
|
|
154
|
+
else:
|
|
155
|
+
h = 60 * (((r - g) / diff) + 4)
|
|
156
|
+
|
|
157
|
+
if h < 0:
|
|
158
|
+
h += 360
|
|
159
|
+
|
|
160
|
+
return h, s, v
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def load_template(name: str) -> Optional[np.ndarray]:
|
|
164
|
+
"""加载模板图片(灰度)"""
|
|
165
|
+
path = os.path.join(TEMPLATE_DIR, f"{name}.png")
|
|
166
|
+
if not os.path.exists(path):
|
|
167
|
+
return None
|
|
168
|
+
return cv2.imread(path, cv2.IMREAD_GRAYSCALE)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def find_button_by_template(screenshot_gray: np.ndarray, template: np.ndarray,
|
|
172
|
+
threshold: float = 0.7) -> Optional[Tuple[int, int, float]]:
|
|
173
|
+
"""
|
|
174
|
+
使用模板匹配在截图中查找按钮
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
screenshot_gray: 灰度截图
|
|
178
|
+
template: 灰度模板
|
|
179
|
+
threshold: 匹配阈值
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
(x, y, confidence) 或 None
|
|
183
|
+
"""
|
|
184
|
+
if template is None:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
result = cv2.matchTemplate(screenshot_gray, template, cv2.TM_CCOEFF_NORMED)
|
|
188
|
+
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
|
|
189
|
+
|
|
190
|
+
if max_val >= threshold:
|
|
191
|
+
return (max_loc[0], max_loc[1], max_val)
|
|
192
|
+
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def analyze_button_color(pil_image: Image.Image, x: int, y: int,
|
|
197
|
+
width: int, height: int, button_type: str = None) -> Tuple[ButtonState, str]:
|
|
198
|
+
"""
|
|
199
|
+
分析按钮区域的颜色来判断状态
|
|
200
|
+
|
|
201
|
+
对于暂停按钮,使用亮度判断(竖线颜色):
|
|
202
|
+
- 浅灰色 (亮度 > 120) = disabled (游戏停止)
|
|
203
|
+
- 深灰色 (亮度 < 120) = enabled (游戏运行中)
|
|
204
|
+
|
|
205
|
+
对于其他按钮,使用彩色判断:
|
|
206
|
+
- 有彩色 = enabled
|
|
207
|
+
- 灰色 = disabled
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
(state, color_type)
|
|
211
|
+
"""
|
|
212
|
+
# 饱和度阈值
|
|
213
|
+
COLOR_THRESHOLD = 50
|
|
214
|
+
|
|
215
|
+
red_count = 0
|
|
216
|
+
green_count = 0
|
|
217
|
+
blue_count = 0
|
|
218
|
+
dark_pixel_count = 0 # 深色像素计数
|
|
219
|
+
total = 0
|
|
220
|
+
total_dark_brightness = 0 # 深色像素的亮度总和
|
|
221
|
+
|
|
222
|
+
for dy in range(height):
|
|
223
|
+
for dx in range(width):
|
|
224
|
+
try:
|
|
225
|
+
r, g, b = pil_image.getpixel((x + dx, y + dy))[:3]
|
|
226
|
+
h, s, v = rgb_to_hsv(r, g, b)
|
|
227
|
+
brightness = (r + g + b) / 3
|
|
228
|
+
total += 1
|
|
229
|
+
|
|
230
|
+
# 统计非白色像素(按钮图标本身)
|
|
231
|
+
if brightness < 200:
|
|
232
|
+
dark_pixel_count += 1
|
|
233
|
+
total_dark_brightness += brightness
|
|
234
|
+
|
|
235
|
+
if s > COLOR_THRESHOLD and v > 30:
|
|
236
|
+
if h <= 30 or h >= 330: # 红色
|
|
237
|
+
red_count += 1
|
|
238
|
+
elif 80 <= h <= 160: # 绿色
|
|
239
|
+
green_count += 1
|
|
240
|
+
elif 180 <= h <= 250: # 蓝色
|
|
241
|
+
blue_count += 1
|
|
242
|
+
except:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
if total == 0:
|
|
246
|
+
return ButtonState.UNKNOWN, "unknown"
|
|
247
|
+
|
|
248
|
+
# 暂停按钮特殊处理:用图标亮度判断
|
|
249
|
+
if button_type == "pause" and dark_pixel_count > 0:
|
|
250
|
+
avg_dark_brightness = total_dark_brightness / dark_pixel_count
|
|
251
|
+
# 亮度 > 120 = 浅灰色 = disabled (游戏停止)
|
|
252
|
+
# 亮度 < 120 = 深灰色 = enabled (游戏运行中)
|
|
253
|
+
if avg_dark_brightness < 120:
|
|
254
|
+
return ButtonState.ENABLED, "dark"
|
|
255
|
+
else:
|
|
256
|
+
return ButtonState.DISABLED, "light"
|
|
257
|
+
|
|
258
|
+
# 其他按钮:用彩色判断
|
|
259
|
+
min_ratio = 0.03
|
|
260
|
+
if red_count > total * min_ratio:
|
|
261
|
+
return ButtonState.ENABLED, "red"
|
|
262
|
+
elif green_count > total * min_ratio:
|
|
263
|
+
return ButtonState.ENABLED, "green"
|
|
264
|
+
elif blue_count > total * min_ratio:
|
|
265
|
+
return ButtonState.ENABLED, "blue"
|
|
266
|
+
else:
|
|
267
|
+
return ButtonState.DISABLED, "gray"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def restore_window_if_minimized(hwnd: int) -> bool:
|
|
271
|
+
"""
|
|
272
|
+
如果窗口最小化,则恢复窗口
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
hwnd: 窗口句柄
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
True 如果窗口已恢复或本来就不是最小化
|
|
279
|
+
"""
|
|
280
|
+
import ctypes
|
|
281
|
+
import time
|
|
282
|
+
|
|
283
|
+
# 检查是否最小化
|
|
284
|
+
is_iconic = ctypes.windll.user32.IsIconic(hwnd)
|
|
285
|
+
if is_iconic:
|
|
286
|
+
# SW_RESTORE = 9
|
|
287
|
+
ctypes.windll.user32.ShowWindow(hwnd, 9)
|
|
288
|
+
time.sleep(0.3) # 等待窗口恢复
|
|
289
|
+
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def detect_toolbar_state(hwnd: int) -> Optional[ToolbarState]:
|
|
294
|
+
"""
|
|
295
|
+
检测工具栏按钮状态
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
hwnd: Roblox Studio 窗口句柄
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
ToolbarState 对象,失败返回 None
|
|
302
|
+
"""
|
|
303
|
+
# 如果窗口最小化,先恢复
|
|
304
|
+
restore_window_if_minimized(hwnd)
|
|
305
|
+
|
|
306
|
+
# 截取窗口
|
|
307
|
+
pil_image = capture_window_to_image(hwnd)
|
|
308
|
+
if pil_image is None:
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
# 转换为 OpenCV 格式
|
|
312
|
+
cv_image = pil_to_cv2(pil_image)
|
|
313
|
+
gray_image = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
|
|
314
|
+
|
|
315
|
+
# 加载模板
|
|
316
|
+
play_template = load_template("play")
|
|
317
|
+
pause_template = load_template("pause")
|
|
318
|
+
stop_template = load_template("stop")
|
|
319
|
+
|
|
320
|
+
buttons = []
|
|
321
|
+
play_state = ButtonState.UNKNOWN
|
|
322
|
+
pause_state = ButtonState.UNKNOWN
|
|
323
|
+
stop_state = ButtonState.UNKNOWN
|
|
324
|
+
|
|
325
|
+
# 查找播放按钮
|
|
326
|
+
if play_template is not None:
|
|
327
|
+
result = find_button_by_template(gray_image, play_template)
|
|
328
|
+
if result:
|
|
329
|
+
x, y, confidence = result
|
|
330
|
+
h, w = play_template.shape
|
|
331
|
+
state, color = analyze_button_color(pil_image, x, y, w, h)
|
|
332
|
+
play_state = state
|
|
333
|
+
buttons.append(ButtonInfo(
|
|
334
|
+
button_type=ButtonType.PLAY,
|
|
335
|
+
x=x, y=y, width=w, height=h,
|
|
336
|
+
state=state, color_type=color, confidence=confidence
|
|
337
|
+
))
|
|
338
|
+
|
|
339
|
+
# 查找暂停按钮
|
|
340
|
+
if pause_template is not None:
|
|
341
|
+
result = find_button_by_template(gray_image, pause_template)
|
|
342
|
+
if result:
|
|
343
|
+
x, y, confidence = result
|
|
344
|
+
h, w = pause_template.shape
|
|
345
|
+
state, color = analyze_button_color(pil_image, x, y, w, h, button_type="pause")
|
|
346
|
+
pause_state = state
|
|
347
|
+
buttons.append(ButtonInfo(
|
|
348
|
+
button_type=ButtonType.PAUSE,
|
|
349
|
+
x=x, y=y, width=w, height=h,
|
|
350
|
+
state=state, color_type=color, confidence=confidence
|
|
351
|
+
))
|
|
352
|
+
|
|
353
|
+
# 查找停止按钮
|
|
354
|
+
if stop_template is not None:
|
|
355
|
+
result = find_button_by_template(gray_image, stop_template)
|
|
356
|
+
if result:
|
|
357
|
+
x, y, confidence = result
|
|
358
|
+
h, w = stop_template.shape
|
|
359
|
+
state, color = analyze_button_color(pil_image, x, y, w, h)
|
|
360
|
+
stop_state = state
|
|
361
|
+
buttons.append(ButtonInfo(
|
|
362
|
+
button_type=ButtonType.STOP,
|
|
363
|
+
x=x, y=y, width=w, height=h,
|
|
364
|
+
state=state, color_type=color, confidence=confidence
|
|
365
|
+
))
|
|
366
|
+
|
|
367
|
+
# 推断游戏状态
|
|
368
|
+
game_state = infer_game_state(play_state, pause_state, stop_state)
|
|
369
|
+
|
|
370
|
+
return ToolbarState(
|
|
371
|
+
play=play_state,
|
|
372
|
+
pause=pause_state,
|
|
373
|
+
stop=stop_state,
|
|
374
|
+
game_state=game_state,
|
|
375
|
+
buttons=buttons
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def infer_game_state(play: ButtonState, pause: ButtonState, stop: ButtonState) -> str:
|
|
380
|
+
"""
|
|
381
|
+
根据按钮状态推断游戏状态
|
|
382
|
+
|
|
383
|
+
规则(基于实际观察):
|
|
384
|
+
- 暂停按钮深灰色(enabled) -> 游戏运行中
|
|
385
|
+
- 暂停按钮浅灰色(disabled) -> 游戏停止
|
|
386
|
+
"""
|
|
387
|
+
if pause == ButtonState.ENABLED:
|
|
388
|
+
# 暂停按钮深灰色 = 游戏运行中
|
|
389
|
+
return "running"
|
|
390
|
+
else:
|
|
391
|
+
# 暂停按钮浅灰色 = 游戏停止
|
|
392
|
+
return "stopped"
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def detect_toolbar_state_with_debug(hwnd: int, debug_output_path: str = None) -> Tuple[Optional[ToolbarState], dict]:
|
|
396
|
+
"""
|
|
397
|
+
检测工具栏状态(带调试信息)
|
|
398
|
+
"""
|
|
399
|
+
debug_info = {}
|
|
400
|
+
|
|
401
|
+
# 如果窗口最小化,先恢复
|
|
402
|
+
restore_window_if_minimized(hwnd)
|
|
403
|
+
|
|
404
|
+
# 截取窗口
|
|
405
|
+
pil_image = capture_window_to_image(hwnd)
|
|
406
|
+
if pil_image is None:
|
|
407
|
+
debug_info["error"] = "Failed to capture window"
|
|
408
|
+
return None, debug_info
|
|
409
|
+
|
|
410
|
+
debug_info["window_size"] = pil_image.size
|
|
411
|
+
|
|
412
|
+
# 转换为 OpenCV 格式
|
|
413
|
+
cv_image = pil_to_cv2(pil_image)
|
|
414
|
+
gray_image = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
|
|
415
|
+
|
|
416
|
+
# 加载模板
|
|
417
|
+
play_template = load_template("play")
|
|
418
|
+
pause_template = load_template("pause")
|
|
419
|
+
stop_template = load_template("stop")
|
|
420
|
+
|
|
421
|
+
debug_info["templates_loaded"] = {
|
|
422
|
+
"play": play_template is not None,
|
|
423
|
+
"pause": pause_template is not None,
|
|
424
|
+
"stop": stop_template is not None
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
buttons = []
|
|
428
|
+
play_state = ButtonState.UNKNOWN
|
|
429
|
+
pause_state = ButtonState.UNKNOWN
|
|
430
|
+
stop_state = ButtonState.UNKNOWN
|
|
431
|
+
|
|
432
|
+
# 查找播放按钮
|
|
433
|
+
if play_template is not None:
|
|
434
|
+
result = find_button_by_template(gray_image, play_template)
|
|
435
|
+
if result:
|
|
436
|
+
x, y, confidence = result
|
|
437
|
+
h, w = play_template.shape
|
|
438
|
+
state, color = analyze_button_color(pil_image, x, y, w, h)
|
|
439
|
+
play_state = state
|
|
440
|
+
buttons.append(ButtonInfo(
|
|
441
|
+
button_type=ButtonType.PLAY,
|
|
442
|
+
x=x, y=y, width=w, height=h,
|
|
443
|
+
state=state, color_type=color, confidence=confidence
|
|
444
|
+
))
|
|
445
|
+
debug_info["play"] = {"x": x, "y": y, "confidence": confidence, "state": state.value, "color": color}
|
|
446
|
+
else:
|
|
447
|
+
debug_info["play"] = {"error": "not found"}
|
|
448
|
+
|
|
449
|
+
# 查找暂停按钮
|
|
450
|
+
if pause_template is not None:
|
|
451
|
+
result = find_button_by_template(gray_image, pause_template)
|
|
452
|
+
if result:
|
|
453
|
+
x, y, confidence = result
|
|
454
|
+
h, w = pause_template.shape
|
|
455
|
+
state, color = analyze_button_color(pil_image, x, y, w, h, button_type="pause")
|
|
456
|
+
pause_state = state
|
|
457
|
+
buttons.append(ButtonInfo(
|
|
458
|
+
button_type=ButtonType.PAUSE,
|
|
459
|
+
x=x, y=y, width=w, height=h,
|
|
460
|
+
state=state, color_type=color, confidence=confidence
|
|
461
|
+
))
|
|
462
|
+
debug_info["pause"] = {"x": x, "y": y, "confidence": confidence, "state": state.value, "color": color}
|
|
463
|
+
else:
|
|
464
|
+
debug_info["pause"] = {"error": "not found"}
|
|
465
|
+
|
|
466
|
+
# 查找停止按钮
|
|
467
|
+
if stop_template is not None:
|
|
468
|
+
result = find_button_by_template(gray_image, stop_template)
|
|
469
|
+
if result:
|
|
470
|
+
x, y, confidence = result
|
|
471
|
+
h, w = stop_template.shape
|
|
472
|
+
state, color = analyze_button_color(pil_image, x, y, w, h)
|
|
473
|
+
stop_state = state
|
|
474
|
+
buttons.append(ButtonInfo(
|
|
475
|
+
button_type=ButtonType.STOP,
|
|
476
|
+
x=x, y=y, width=w, height=h,
|
|
477
|
+
state=state, color_type=color, confidence=confidence
|
|
478
|
+
))
|
|
479
|
+
debug_info["stop"] = {"x": x, "y": y, "confidence": confidence, "state": state.value, "color": color}
|
|
480
|
+
else:
|
|
481
|
+
debug_info["stop"] = {"error": "not found"}
|
|
482
|
+
|
|
483
|
+
# 保存调试图像
|
|
484
|
+
if debug_output_path:
|
|
485
|
+
debug_img = cv_image.copy()
|
|
486
|
+
colors = {
|
|
487
|
+
ButtonType.PLAY: (0, 255, 0), # 绿色
|
|
488
|
+
ButtonType.PAUSE: (255, 255, 0), # 青色
|
|
489
|
+
ButtonType.STOP: (0, 0, 255) # 红色
|
|
490
|
+
}
|
|
491
|
+
for btn in buttons:
|
|
492
|
+
color = colors.get(btn.button_type, (255, 255, 255))
|
|
493
|
+
cv2.rectangle(debug_img, (btn.x, btn.y),
|
|
494
|
+
(btn.x + btn.width, btn.y + btn.height), color, 2)
|
|
495
|
+
label = f"{btn.button_type.value}:{btn.color_type}"
|
|
496
|
+
cv2.putText(debug_img, label, (btn.x, btn.y - 5),
|
|
497
|
+
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
|
|
498
|
+
cv2.imwrite(debug_output_path, debug_img)
|
|
499
|
+
debug_info["debug_image"] = debug_output_path
|
|
500
|
+
|
|
501
|
+
# 推断游戏状态
|
|
502
|
+
game_state = infer_game_state(play_state, pause_state, stop_state)
|
|
503
|
+
debug_info["game_state"] = game_state
|
|
504
|
+
|
|
505
|
+
toolbar_state = ToolbarState(
|
|
506
|
+
play=play_state,
|
|
507
|
+
pause=pause_state,
|
|
508
|
+
stop=stop_state,
|
|
509
|
+
game_state=game_state,
|
|
510
|
+
buttons=buttons
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
return toolbar_state, debug_info
|