1
0
mirror of https://github.com/sui-feng-cb/AzurLaneAutoScript1.git synced 2026-08-14 19:36:38 +08:00

Merge branch 'dev'

This commit is contained in:
2026-08-12 15:36:15 +08:00
30 changed files with 843 additions and 32 deletions

View File

@@ -176,7 +176,7 @@ class AlOcr(CnOcr):
prefix = os.path.join(self._model_dir, self._model_file_prefix)
data_names = ['data']
data_shapes = [(data_names[0], (hp.batch_size, 1, hp.img_height, hp.img_width))]
logger.info('Loading OCR model: %s' % self._model_dir) # Change log appearance.
logger.info('Loading OCR model (MXNET): %s' % self._model_dir) # Change log appearance.
mod = load_module(
prefix,
self._model_epoch,
@@ -235,3 +235,79 @@ class AlOcr(CnOcr):
img_list, img_widths = self._pad_arrays(img_list)
image = cv2.hconcat(img_list)[0, :, :]
Image.fromarray(image).show()
class _OnnxModule:
"""
Wrapper presenting the MXNet Module `predict(sample)` interface,
but returning numpy arrays to avoid the MXNet NDArray round trip.
"""
def __init__(self, session):
self._session = session
self._input_name = session.get_inputs()[0].name
def predict(self, sample):
import mxnet as mx
if isinstance(sample, mx.nd.NDArray):
sample = sample.asnumpy()
# sample: (batch, 1, 32, width) -> output: (seq_len * batch, num_classes)
out = self._session.run(None, {self._input_name: np.asarray(sample, dtype=np.float32)})[0]
return out
class AlOcrOnnx(AlOcr):
"""
Subclass of AlOcr that runs inference through ONNX Runtime instead of MXNet,
providing faster CPU inference with numerically identical output.
All pre/post-processing is inherited unchanged.
Requires onnxruntime installed and model.onnx in the model directory
(generated by dev_tools/convert_mxnet_to_onnx.py from the MXNet checkpoint).
Falls back to MXNet if either is missing.
NOTE: For fully dropping MXNet,
should vendor the pure-Python components of cnocr 1.2.2,
and remove the mxnet/gluoncv dependencies.
"""
def _get_module(self, context):
_network, self._hp = gen_network(self._model_name, self._hp, self._net_prefix)
onnx_path = os.path.join(self._model_dir, 'model.onnx')
if not os.path.exists(onnx_path):
logger.warning(f'ONNX model not found: {onnx_path}, '
'fall back to MXNet')
return AlOcr._get_module(self, context)
try:
import onnxruntime as ort
except ImportError:
logger.warning('onnxruntime not installed, fall back to MXNet')
return AlOcr._get_module(self, context)
logger.info(f'Loading OCR model (ONNX): {onnx_path}')
so = ort.SessionOptions()
from module.webui.setting import State
threads = State.deploy_config.IntraOpThreads
so.intra_op_num_threads = threads if isinstance(threads, int) and threads > 0 else 0
session = ort.InferenceSession(onnx_path, so, providers=['CPUExecutionProvider'])
return _OnnxModule(session)
def _predict(self, sample):
"""
Args:
sample (np.ndarray or mx.nd.NDArray): (batch, 1, 32, width), float32.
Returns:
np.ndarray: (seq_len * batch, num_classes)
"""
if isinstance(self._mod, _OnnxModule):
return self._mod.predict(sample)
else:
import mxnet as mx
# MXNet module expects NDArray
if not isinstance(sample, mx.nd.NDArray):
sample = mx.nd.array(sample)
return super()._predict(sample)

View File

@@ -2,64 +2,76 @@ from module.base.decorator import cached_property
class OcrModel:
@cached_property
def _OCR(self):
from module.webui.setting import State
backend = State.deploy_config.OcrBackend
if backend == 'onnx':
from module.ocr.al_ocr import AlOcrOnnx as _OCR
else:
from module.ocr.al_ocr import AlOcr as _OCR
return _OCR
@cached_property
def azur_lane(self):
# Folder: ./bin/cnocr_models/azur_lane
# Size: 3.25MB
# Size: 3.25MB (MXNet) / 3.34MB (ONNX)
# Model: densenet-lite-gru
# Epoch: 15
# Validation accuracy: 99.43%
# Font: Impact, AgencyFB-Regular, MStiffHeiHK-UltraBold
# Charset: 0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ:/- (Letter 'O' and <space> is not included)
# _num_classes: 39
from module.ocr.al_ocr import AlOcr
return AlOcr(model_name='densenet-lite-gru', model_epoch=15, root='./bin/cnocr_models/azur_lane',
name='azur_lane')
return self._OCR(model_name='densenet-lite-gru', model_epoch=15,
root='./bin/cnocr_models/azur_lane', name='azur_lane')
@cached_property
def azur_lane_jp(self):
# Folder: ./bin/cnocr_models/azur_lane_jp
# Size: 3.25MB
# Size: 3.25MB (MXNet) / 3.34MB (ONNX)
# Model: densenet-lite-gru
# Epoch: 20
# Validation accuracy: 99.01%
# Font: Impact, VibeMO Compressed Pro Thin, Folk R, Source Han Serif JP
# Charset: 0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ:/- (Letter 'O' and <space> is not included)
# _num_classes: 39
from module.ocr.al_ocr import AlOcr
return AlOcr(model_name='densenet-lite-gru', model_epoch=20, root='./bin/cnocr_models/azur_lane_jp',
name='azur_lane_jp')
return self._OCR(model_name='densenet-lite-gru', model_epoch=20,
root='./bin/cnocr_models/azur_lane_jp', name='azur_lane_jp')
@cached_property
def cnocr(self):
# Folder: ./bin/cnocr_models/cnocr
# Size: 9.51MB
# Size: 9.51MB (MXNet) / 9.75MB (ONNX)
# Model: densenet-lite-gru
# Epoch: 39
# Validation accuracy: 99.04%
# Font: Various
# Charset: Number, English character, Chinese character, symbols, <space>
# _num_classes: 6426
from module.ocr.al_ocr import AlOcr
return AlOcr(model_name='densenet-lite-gru', model_epoch=39, root='./bin/cnocr_models/cnocr', name='cnocr')
return self._OCR(model_name='densenet-lite-gru', model_epoch=39,
root='./bin/cnocr_models/cnocr', name='cnocr')
@cached_property
def jp(self):
from module.ocr.al_ocr import AlOcr
return AlOcr(model_name='densenet-lite-gru', model_epoch=125, root='./bin/cnocr_models/jp', name='jp')
# Folder: ./bin/cnocr_models/jp
# Size: 6.36MB (ONNX)
# Model: densenet-lite-gru
# Epoch: 125
return self._OCR(model_name='densenet-lite-gru', model_epoch=125,
root='./bin/cnocr_models/jp', name='jp')
@cached_property
def tw(self):
# Folder: ./bin/cnocr_models/tw
# Size: 8.43MB
# Size: 8.43MB (MXNet) / 8.64MB (ONNX)
# Model: densenet-lite-gru
# Epoch: 63
# Validation accuracy: 99.24%
# Font: Various, 6 kinds
# Charset: Numbers, Upper english characters, Chinese traditional characters
# _num_classes: 5322
from module.ocr.al_ocr import AlOcr
return AlOcr(model_name='densenet-lite-gru', model_epoch=63, root='./bin/cnocr_models/tw', name='tw')
return self._OCR(model_name='densenet-lite-gru', model_epoch=63,
root='./bin/cnocr_models/tw', name='tw')
OCR_MODEL = OcrModel()

View File

@@ -1012,12 +1012,36 @@ class OSMap(OSFleet, Map, GlobeCamera, StorageHandler, StrategicSearchHandler):
self.clear_question()
self.map_rescan()
def _swipe_camera_avoid_ui(self, grid, ref=None, _ui_avoid_count=0, name='Grid'):
"""
Swipe camera to move a grid that overlaps with map options area out of it to avoid misclicks.
Args:
grid (OSGrid): Grid to check
ref (tuple): Grid location to measure the swipe vector from,
defaults to the screen center (view.center_loca)
_ui_avoid_count (int): Internal counter for map options area avoidance attempts
name (str): Grid event name, used in log message
Returns:
bool: True if the camera swiped and the map should be rescanned; False if the grid is safe to click.
"""
if _ui_avoid_count < 3 and area_cross_area(grid.button, MAP_OPTIONS_AREA.area, threshold=0):
if ref is None:
ref = self.view.center_loca
vector = np.array(grid.location) - np.array(ref)
vector = tuple((vector // 2).astype(int))
logger.info(f'{name} grid {grid} overlaps with map options area, swipe {vector}')
self.map_swipe(vector)
return True
return False
def map_rescan_current(self, drop=None, _ui_avoid_count=0):
"""
Args:
drop:
_ui_avoid_count (int): Internal counter for map options area avoidance attempts
_ui_avoid_count (int):
Returns:
bool: If solved a map random event
@@ -1055,12 +1079,7 @@ class OSMap(OSFleet, Map, GlobeCamera, StorageHandler, StrategicSearchHandler):
logger.info(f'Found Akashi on {grid}')
fleet = self.convert_radar_to_local((0, 0))
if fleet.distance_to(grid) > 1:
# Avoid clicking grid that overlaps with map options area
if _ui_avoid_count < 3 and area_cross_area(grid.button, MAP_OPTIONS_AREA.area, threshold=0):
logger.info(f'Akashi grid {grid} overlaps with map options area, swipe camera to avoid')
vector = np.array(grid.location) - np.array(fleet.location)
vector = tuple((vector // 2).astype(int))
self.map_swipe(vector)
if self._swipe_camera_avoid_ui(grid, ref=fleet.location, _ui_avoid_count=_ui_avoid_count, name='Akashi'):
return self.map_rescan_current(drop=drop, _ui_avoid_count=_ui_avoid_count + 1)
self.device.click(grid)
with self.config.temporary(STORY_ALLOW_SKIP=False):
@@ -1087,6 +1106,8 @@ class OSMap(OSFleet, Map, GlobeCamera, StorageHandler, StrategicSearchHandler):
self._solved_map_event.add('is_scanning_device')
return True
if self._swipe_camera_avoid_ui(grid, _ui_avoid_count=_ui_avoid_count, name='Scanning device'):
return self.map_rescan_current(drop=drop, _ui_avoid_count=_ui_avoid_count + 1)
self.device.click(grid)
with self.config.temporary(STORY_ALLOW_SKIP=False):
result = self.wait_until_walk_stable(
@@ -1102,6 +1123,8 @@ class OSMap(OSFleet, Map, GlobeCamera, StorageHandler, StrategicSearchHandler):
if 'is_logging_tower' not in self._solved_map_event and grids and grids[0].is_logging_tower:
grid = grids[0]
logger.info(f'Found logging tower on {grid}')
if self._swipe_camera_avoid_ui(grid, _ui_avoid_count=_ui_avoid_count, name='Logging tower'):
return self.map_rescan_current(drop=drop, _ui_avoid_count=_ui_avoid_count + 1)
self.device.click(grid)
with self.config.temporary(STORY_ALLOW_SKIP=False):
result = self.wait_until_walk_stable(
@@ -1119,6 +1142,8 @@ class OSMap(OSFleet, Map, GlobeCamera, StorageHandler, StrategicSearchHandler):
and grids[0].is_fleet_mechanism:
grid = grids[0]
logger.info(f'Found fleet mechanism on {grid}')
if self._swipe_camera_avoid_ui(grid, _ui_avoid_count=_ui_avoid_count, name='Fleet mechanism'):
return self.map_rescan_current(drop=drop, _ui_avoid_count=_ui_avoid_count + 1)
self.device.click(grid)
self.wait_until_walk_stable(drop=drop, walk_out_of_step=False, confirm_timer=Timer(1.5, count=4))

View File

@@ -17,7 +17,7 @@ class OpsiHazard1Leveling(OSMap):
self.config.OS_ACTION_POINT_PRESERVE = 200
if self.config.is_task_enabled('OpsiAshBeacon') \
and not self._ash_fully_collected \
and self.config.OpsiAshBeacon_EnsureFullyCollected:
and self.config.cross_get(keys='OpsiAshBeacon.OpsiAshBeacon.EnsureFullyCollected', default=True):
logger.info('Ash beacon not fully collected, ignore action point limit temporarily')
self.config.OS_ACTION_POINT_PRESERVE = 0
logger.attr('OS_ACTION_POINT_PRESERVE', self.config.OS_ACTION_POINT_PRESERVE)

View File

@@ -44,7 +44,7 @@ class OpsiMeowfficerFarming(OSMap):
self.config.OS_ACTION_POINT_PRESERVE = preserve
if self.config.is_task_enabled('OpsiAshBeacon') \
and not self._ash_fully_collected \
and self.config.OpsiAshBeacon_EnsureFullyCollected:
and self.config.cross_get(keys='OpsiAshBeacon.OpsiAshBeacon.EnsureFullyCollected', default=True):
logger.info('Ash beacon not fully collected, ignore action point limit temporarily')
self.config.OS_ACTION_POINT_PRESERVE = 0
logger.attr('OS_ACTION_POINT_PRESERVE', self.config.OS_ACTION_POINT_PRESERVE)