Skip to content

Commit c72ed6a

Browse files
authored
fix: decisive node recognize (#409)
* fix: node recognize * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update map_controller.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update map_controller.py --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> fix #408
1 parent 2a9e679 commit c72ed6a

3 files changed

Lines changed: 92 additions & 191 deletions

File tree

autowsgr/ops/decisive/handlers.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,7 @@ def _handle_prepare_combat(self) -> None:
259259
self._state.node = 'A'
260260
_log.info('[决战] 首次进入第 1 小节,跳过节点识别并默认使用节点 A')
261261
else:
262-
self._state.node = self._map.recognize_node(
263-
screen,
264-
chapter=self._config.chapter,
265-
stage=self._state.stage,
266-
)
262+
self._state.node = self._map.recognize_node()
267263
_log.info(
268264
'[决战] 出征准备 (小关 {} 节点 {})',
269265
self._state.stage,
@@ -287,13 +283,12 @@ def _handle_prepare_combat(self) -> None:
287283
skill_used = self._map.is_skill_used()
288284
_log.debug('[决战] 节点: {}, 技能已使用检测: {}', current_node, skill_used)
289285

290-
# 强制使用技能条件:节点 A/U 且首次进入(尚未选择过舰队)
291-
should_use_skill = (
292-
current_node == 'A' or current_node == 'U'
293-
) and not self._has_chosen_fleet
286+
# 强制使用技能条件:节点 A/U 且首次进入(已经选择过舰队)
287+
should_use_skill = (current_node == 'A' or current_node == 'U') and self._has_chosen_fleet
294288

295289
if should_use_skill or ((current_node == 'A' or current_node == 'U') and not skill_used):
296290
_log.debug('[决战] 执行技能使用: 强制={}', should_use_skill)
291+
time.sleep(0.5)
297292
gained = self._map.use_skill()
298293
if gained:
299294
if self._config.useful_skill and not self._logic.check_useful_skill(gained):

autowsgr/ui/decisive/battle_page.py

Lines changed: 31 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -107,55 +107,26 @@
107107
"""章节导航最大尝试次数。"""
108108

109109
MAX_CHAPTER: int = 6
110-
MIN_CHAPTER: int = 1
111-
112-
# ── recognize_stage 检测参数 ──
113-
114-
_STAGES_CHECK: dict[int, dict] = {
115-
1: {
116-
'points': [(0.4479, 0.3269), (0.6906, 0.3769)], # 占位,待手动提取
117-
'color': Color.of(249, 232, 94), # 黄色 - 待确认
118-
'tolerance': 40.0, # 待调整
119-
},
120-
2: {
121-
'points': [(0.4672, 0.3250), (0.6099, 0.6056)], # 已完成 - 用户提取
122-
'color': Color.of(249, 232, 94), # 黄色 - 已确认
123-
'tolerance': 40.0, # 已调整
124-
},
125-
3: {
126-
'points': [(0.4510, 0.6065), (0.6833, 0.3361)], # 已完成 - 用户提取
127-
'color': Color.of(249, 232, 94), # 黄色 - 已确认
128-
'tolerance': 40.0, # 已调整
129-
},
130-
4: {
131-
'points': [(0.4130, 0.3769), (0.6281, 0.5769)], # 已完成 - 用户提取
132-
'color': Color.of(249, 232, 94), # 黄色 - 已确认
133-
'tolerance': 40.0, # 已调整
134-
},
135-
5: {
136-
'points': [(0.4484, 0.3269), (0.5813, 0.6954)], # 已完成 - 用户提取
137-
'color': Color.of(249, 232, 94), # 黄色 - 已确认
138-
'tolerance': 40.0, # 已调整
139-
},
140-
6: {
141-
'points': [(0.6396, 0.3074), (0.5609, 0.6287)], # 已完成 - 用户提取
142-
'color': Color.of(249, 232, 94), # 黄色 - 已确认
143-
'tolerance': 40.0, # 已调整
144-
},
145-
}
146-
"""各章节小关检测配置。
110+
MIN_CHAPTER: int = 4
111+
112+
# ── recognize_stage 检测点 ──
147113

148-
每个章节的配置包含:
149-
- points: 前 2 个小关的完成标记检测点列表 (相对坐标)
150-
- color: 完成标记的颜色 (RGB)
151-
- tolerance: 颜色匹配容差
114+
_STAGE_CHECK_POINTS: dict[int, list[tuple[float, float]]] = {
115+
4: [(0.381, 0.436), (0.596, 0.636), (0.778, 0.521)],
116+
5: [(0.418, 0.378), (0.760, 0.477), (0.550, 0.750)],
117+
6: [(0.606, 0.375), (0.532, 0.703), (0.862, 0.644)],
118+
}
119+
"""每章 3 个小关的像素检测点 (相对坐标)。
152120
153-
通过检测前 2 个小关的完成状态推断当前是第几小关:
154-
- 第 1 个点未匹配 → 第 1 小关
155-
- 第 1 个点匹配但第 2 个点未匹配 → 第 2 小关
156-
- 两个点都匹配 → 第 3 小关
121+
若检测点颜色接近白色 (250, 244, 253) 表示该小关已通过。
157122
"""
158123

124+
_STAGE_CHECK_COLOR: Color = Color.of(250, 244, 253)
125+
"""小关已通过标记颜色 (近白色)。"""
126+
127+
_STAGE_CHECK_TOLERANCE: float = 30.0
128+
"""颜色匹配容差。"""
129+
159130

160131
# ═══════════════════════════════════════════════════════════════════════════════
161132
# 页面控制器
@@ -196,32 +167,24 @@ def is_current_page(screen: np.ndarray) -> bool:
196167

197168
@staticmethod
198169
def recognize_stage(screen: np.ndarray, chapter: int) -> int:
199-
"""识别当前决战章节的小关进度 (1-3)。
170+
"""识别当前决战章节的小关进度 (0-3)。
200171
201-
使用递进式判定逻辑:通过检测前 2 个小关的完成状态推断当前小关
202-
所有章节使用统一的配置结构,只是参数不同
172+
检查每个小关位置像素颜色,白色 (250,244,253) 为已通过
173+
返回当前正在进行的小关编号; 3 表示全部通过
203174
"""
204-
config = _STAGES_CHECK.get(chapter)
205-
if config is None:
206-
_log.warning('[决战] 未知章节 {}', chapter)
207-
return 1
208-
209-
check_points = config['points']
210-
check_color = config['color']
211-
check_tolerance = config['tolerance']
212-
213-
for i, (rx, ry) in enumerate(check_points, start=1):
214-
matched = PixelChecker.check_pixel(screen, rx, ry, check_color, check_tolerance)
215-
actual_color = PixelChecker.get_pixel(screen, rx, ry)
216-
_log.debug(
217-
'[决战] 小关 {} 检测点 ({:.3f}, {:.3f}): 匹配={}, 实际颜色={}',
218-
i,
175+
check_points = _STAGE_CHECK_POINTS.get(chapter)
176+
if check_points is None:
177+
_log.warning('[决战] 决战 recognize_stage: 未知章节 {}', chapter)
178+
return 0
179+
180+
for i, (rx, ry) in enumerate(check_points):
181+
if not PixelChecker.check_pixel(
182+
screen,
219183
rx,
220184
ry,
221-
matched,
222-
actual_color.as_rgb_tuple(),
223-
)
224-
if not matched:
185+
_STAGE_CHECK_COLOR,
186+
_STAGE_CHECK_TOLERANCE,
187+
):
225188
_log.info('[决战] 识别决战地图参数, 第 {} 小节正在进行', i)
226189
return i
227190

@@ -303,7 +266,7 @@ def navigate_to_chapter(self, target: int) -> None:
303266
Parameters
304267
----------
305268
target:
306-
目标章节编号 (1-6)。
269+
目标章节编号 (MIN_CHAPTER - MAX_CHAPTER)。
307270
308271
Raises
309272
------

autowsgr/ui/decisive/map_controller.py

Lines changed: 57 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
from __future__ import annotations
1515

16-
import re
1716
import time
1817
from typing import TYPE_CHECKING
1918

@@ -48,8 +47,8 @@
4847
PixelChecker,
4948
PixelRule,
5049
PixelSignature,
50+
get_api_dll,
5151
)
52-
from autowsgr.vision.ocr import OCREngine
5352

5453
from ..page import click_and_wait_for_page
5554

@@ -223,41 +222,31 @@ def _locate_ship_icon(bgr: np.ndarray) -> float | None:
223222
return None
224223
return float(centroids[best][0]) / bgr.shape[1]
225224

225+
def dll_recognize_map(self, dll, screen, center) -> str:
226+
h, w = screen.shape[:2]
227+
x1 = max(0, int((center - 0.03) * w))
228+
x2 = min(w, int((center - 0.03 + 0.042) * w))
229+
col_crop = screen[0:h, x1:x2]
230+
result = dll.recognize_map(col_crop)
231+
# save_image(col_crop, result + '.png')
232+
return result
233+
226234
def recognize_node(
227235
self,
228-
screen: np.ndarray | None = None,
229-
fallback: str = 'A',
230-
chapter: int = 6,
231-
stage: int = 1,
232236
) -> str:
233-
"""OCR 识别当前决战节点字母 (如 ``'A'``, ``'B'``)。
237+
"""DLL 识别当前决战节点字母 (如 ``'A'``, ``'B'``)。
234238
235239
算法 (无模板依赖):
236240
237241
1. 轮询截图,通过 **HSV 颜色分割** 定位舰船指示器 X 坐标
238242
(地图上最大的橙黄色连通区域)。
239-
2. 以舰标 X 为参考裁剪竖列,使用 OCR 识别节点字母。
240-
3. OCR 失败时重试 (最多 3 次),全部失败返回 fallback。
241-
242-
Parameters
243-
----------
244-
chapter:
245-
当前章节 (1-6),用于确定OCR字符集范围。
246-
stage:
247-
当前小关 (1-3),用于确定OCR字符集范围。
243+
2. 以舰标 X 为参考裁剪全高竖列,送 DLL ``recognize_map`` 识别。
244+
3. DLL 返回 ``'0'`` 时重试 (最多 3 次),全部失败抛出异常。
248245
"""
249-
from autowsgr.ops.decisive.config import MapData
250-
251-
# 根据章节小关确定可能的节点范围 (A到终点)
252-
end_node = MapData.get_stage_end_node(chapter, stage)
253-
end_ord = ord(end_node)
254-
# 生成字符集 A到终点 (如A-F, A-H, A-J)
255-
allowlist = ''.join(chr(i) for i in range(ord('A'), end_ord + 1))
256-
_log.debug('[地图控制器] 章节{}小关{}节点范围: A-{}', chapter, stage, end_node)
257-
258246
_MAX_RETRY = 3
259247
_ICON_TIMEOUT = 10.0
260248
_ICON_GAP = 0.15
249+
dll = get_api_dll()
261250

262251
for retry in range(_MAX_RETRY + 1):
263252
# 1. 轮询等待舰船指示器出现
@@ -276,100 +265,54 @@ def recognize_node(
276265
raise RuntimeError('决战节点识别失败: 舰船指示器超时未出现')
277266

278267
_log.debug('[地图控制器] 舰船指示器位置: X={:.3f}', icon_rel_x)
268+
time.sleep(0.5) # 等待截图稳定
279269

280-
# 2. 取新截图,按舰标 X 裁剪竖列(聚焦节点字母区域)
270+
# 2. 取新截图,按舰标 X 裁剪竖列
281271
fresh_screen = self._ctrl.screenshot()
282-
h, w = fresh_screen.shape[:2]
283-
284-
# 裁剪范围:舰标上方区域(有颜色过滤,范围可以稍宽)
285-
# 节点字母在舰标上方约0.08-0.20处
286-
y1 = int(0.35 * h)
287-
y2 = int(0.55 * h)
288-
x1 = max(0, int((icon_rel_x - 0.05) * w))
289-
x2 = min(w, int((icon_rel_x + 0.05) * w))
290-
node_crop = fresh_screen[y1:y2, x1:x2]
291-
292-
_log.debug(
293-
'[地图控制器] 裁剪区域: Y={}-{}, X={}-{}, shape={}', y1, y2, x1, x2, node_crop.shape
294-
)
295-
296-
# 图像增强:提升对比度和锐化,突出白色文字
297-
# 节点字母颜色近似白色 (RGB 213-231)
298-
gray = cv2.cvtColor(node_crop, cv2.COLOR_RGB2GRAY)
299-
300-
# 自适应直方图均衡化(CLAHE)增强对比度
301-
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
302-
enhanced = clahe.apply(gray)
303-
304-
# 锐化滤波
305-
kernel_sharpen = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
306-
sharpened = cv2.filter2D(enhanced, -1, kernel_sharpen)
307-
308-
# 二值化(使用Otsu自动阈值)
309-
_, binary = cv2.threshold(sharpened, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
310-
311-
# 检查是否有足够的白色像素
312-
white_ratio = np.sum(binary > 0) / binary.size
313-
_log.debug('[地图控制器] 白色像素比例: {:.2%}', white_ratio)
314-
315-
if white_ratio < 0.005: # 白色像素太少,可能没裁到字母
316-
_log.warning('[地图控制器] 白色像素过少,可能未定位到节点字母')
317-
if retry >= _MAX_RETRY:
318-
break
319-
continue
320-
321-
# 使用增强后的图像进行OCR
322-
node_crop_processed = cv2.cvtColor(sharpened, cv2.COLOR_GRAY2RGB)
323-
324-
# 防御性检查
325-
if x1 >= x2 or node_crop.size == 0 or (x2 - x1) < 30:
326-
_log.warning(
327-
'[地图控制器] 裁剪区域无效: x1={}, x2={}, width={}, shape={}',
328-
x1,
329-
x2,
330-
x2 - x1,
331-
node_crop.shape,
332-
)
333-
if retry >= _MAX_RETRY:
334-
break
335-
_log.warning(
336-
'[地图控制器] 节点识别失败, 正在重试第 {} 次',
337-
retry + 1,
338-
)
339-
continue
340272

341-
# 3. OCR 识别节点字母
273+
# 3. DLL 识别
342274
try:
343-
ocr = OCREngine.create('easyocr', gpu=False)
344-
# 使用动态字符集(根据章节小关确定范围)
345-
result = ocr.recognize_single(node_crop_processed, allowlist=allowlist)
346-
text = result.text.strip().upper()
347-
348-
_log.debug(
349-
'[地图控制器] OCR原始结果: "{}" (置信度={:.2f})', text, result.confidence
350-
)
351-
352-
# 提取单个字母(在允许范围内)
353-
match = re.search(rf'[{allowlist}]', text)
354-
if match:
355-
node_letter = match.group(0)
356-
_log.info('[地图控制器] OCR识别节点: {}', node_letter)
357-
return node_letter
358-
else:
359-
_log.warning('[地图控制器] OCR未识别到有效节点字母: "{}"', text)
360-
if retry >= _MAX_RETRY:
361-
break
362-
continue
363-
364-
except Exception as e:
365-
_log.warning('[地图控制器] OCR识别失败: {}', e)
366-
if retry >= _MAX_RETRY:
367-
break
368-
time.sleep(0.5)
369-
continue
275+
result = self.dll_recognize_map(dll, fresh_screen, icon_rel_x)
276+
if result != '0':
277+
_log.info('[地图控制器] 识别决战节点: {}', result[0])
278+
if result[0] == 'C':
279+
right_x = icon_rel_x + 0.172
280+
right_result = self.dll_recognize_map(dll, fresh_screen, right_x)
281+
if right_result == 'D':
282+
result = 'C'
283+
elif right_result == 'C':
284+
result = 'B'
285+
_log.info(
286+
'[地图控制器] C右侧节点识别: {}, 修正后决战节点: {}',
287+
right_result,
288+
result,
289+
)
290+
291+
if result[0] == 'J':
292+
left_x = icon_rel_x - 0.172
293+
left_result = self.dll_recognize_map(dll, fresh_screen, left_x)
294+
if left_result == 'H':
295+
result = 'I'
296+
elif left_result == 'J':
297+
result = 'J'
298+
_log.info(
299+
'[地图控制器] J左侧节点识别: {}, 修正后决战节点: {}',
300+
left_result,
301+
result,
302+
)
303+
304+
return result[0]
305+
except Exception:
306+
_log.warning('[地图控制器] DLL 节点识别异常', exc_info=True)
307+
308+
if retry >= _MAX_RETRY:
309+
break
310+
_log.warning(
311+
'[地图控制器] 节点识别失败, 正在重试第 {} 次',
312+
retry + 1,
313+
)
370314

371-
_log.error('[地图控制器] 节点识别失败,返回 fallback: {}', fallback)
372-
return fallback
315+
raise RuntimeError(f'决战节点识别失败: 重试 {_MAX_RETRY + 1} 次后仍无法识别')
373316

374317
# ══════════════════════════════════════════════════════════════════════
375318
# 战备舰队获取 overlay

0 commit comments

Comments
 (0)