From dca422de8e0c5d9f72ecccd29b64771d7752ec37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=89=E9=B8=AD=E8=9B=8B?= Date: Sun, 19 Apr 2026 00:32:33 +0800 Subject: [PATCH] init --- .../Core/Recognition/ONNX/BgiOnnxFactory.cs | 13 +- .../Core/Recognition/ONNX/BgiOnnxModel.cs | 8 +- .../Recognition/ONNX/BgiRedNetPredictor.cs | 157 ++++++++++++++++++ .../GameTask/AutoPick/AutoPickConfig.cs | 10 +- .../GameTask/AutoPick/AutoPickTrigger.cs | 153 ++++++++++------- .../GameTask/AutoPick/PickOcrEngineEnum.cs | 2 +- .../AutoPick/PickRecognitionModeEnum.cs | 7 + .../View/Pages/TriggerSettingsPage.xaml | 31 +++- .../Pages/TriggerSettingsPageViewModel.cs | 2 + 9 files changed, 319 insertions(+), 64 deletions(-) create mode 100644 BetterGenshinImpact/Core/Recognition/ONNX/BgiRedNetPredictor.cs create mode 100644 BetterGenshinImpact/GameTask/AutoPick/PickRecognitionModeEnum.cs diff --git a/BetterGenshinImpact/Core/Recognition/ONNX/BgiOnnxFactory.cs b/BetterGenshinImpact/Core/Recognition/ONNX/BgiOnnxFactory.cs index 4b30ba2d..c2eba7b5 100644 --- a/BetterGenshinImpact/Core/Recognition/ONNX/BgiOnnxFactory.cs +++ b/BetterGenshinImpact/Core/Recognition/ONNX/BgiOnnxFactory.cs @@ -311,6 +311,17 @@ public class BgiOnnxFactory : new BgiYoloPredictor(model, cached, CreateSessionOptions(model, false)); } + /// + /// 根据模型创建一个 RedNet 分类预测器 + /// + /// 模型 + /// 标签文件相对路径,可选,支持 .txt/.json + /// BgiRedNetPredictor + public BgiRedNetPredictor CreateRedNetPredictor(BgiOnnxModel model, string? labelRelativePath = null) + { + return new BgiRedNetPredictor(model, CreateInferenceSession(model), labelRelativePath); + } + /// /// 根据模型创建一个onnx运行时的InferenceSession /// @@ -582,4 +593,4 @@ public class BgiOnnxFactory result["enable_opencl_throttling"] = "true"; return result; } -} \ No newline at end of file +} diff --git a/BetterGenshinImpact/Core/Recognition/ONNX/BgiOnnxModel.cs b/BetterGenshinImpact/Core/Recognition/ONNX/BgiOnnxModel.cs index fd00f475..4d2040b3 100644 --- a/BetterGenshinImpact/Core/Recognition/ONNX/BgiOnnxModel.cs +++ b/BetterGenshinImpact/Core/Recognition/ONNX/BgiOnnxModel.cs @@ -50,6 +50,12 @@ public class BgiOnnxModel public static readonly BgiOnnxModel BgiAvatarSide = Register("BgiAvatarSide", @"Assets\Model\Common\avatar_side_classify_sim.onnx"); + /// + /// 角色识别(RedNet-110) + /// + public static readonly BgiOnnxModel BgiPickRedNet110 = + Register("BgiPickRedNet110", @"Assets\Model\Common\resnet_pick.onnx"); + /// /// paddleOCR V4 检测模型 /// @@ -139,4 +145,4 @@ public class BgiOnnxModel RegisteredModels.Add(model); return model; } -} \ No newline at end of file +} diff --git a/BetterGenshinImpact/Core/Recognition/ONNX/BgiRedNetPredictor.cs b/BetterGenshinImpact/Core/Recognition/ONNX/BgiRedNetPredictor.cs new file mode 100644 index 00000000..f84228c5 --- /dev/null +++ b/BetterGenshinImpact/Core/Recognition/ONNX/BgiRedNetPredictor.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace BetterGenshinImpact.Core.Recognition.ONNX; + +public sealed class BgiRedNetPredictor : IDisposable +{ + private const int DefaultInputSize = 224; + + private static readonly float[] ImagenetMean = [0.485f, 0.456f, 0.406f]; + private static readonly float[] ImagenetStd = [0.229f, 0.224f, 0.225f]; + + private readonly InferenceSession _session; + private readonly string[]? _labels; + private readonly string _inputName; + private readonly int _inputWidth; + private readonly int _inputHeight; + + /// + /// 使用 BgiOnnxFactory 创建这个类的实例 + /// + internal BgiRedNetPredictor(BgiOnnxModel model, InferenceSession session, string? labelRelativePath = null) + { + _session = session; + + var input = _session.InputMetadata.FirstOrDefault(); + if (input.Key is null || input.Value is null) + { + throw new InvalidDataException("ONNX 模型输入信息为空"); + } + + _inputName = input.Key; + var dimensions = input.Value.Dimensions; + if (dimensions.Length < 4) + { + throw new InvalidDataException($"ONNX 模型输入维度不正确,预期 >=4,实际 {dimensions.Length}"); + } + + _inputHeight = dimensions[^2] > 0 ? dimensions[^2] : DefaultInputSize; + _inputWidth = dimensions[^1] > 0 ? dimensions[^1] : DefaultInputSize; + _labels = LoadLabels(labelRelativePath ?? Path.ChangeExtension(model.ModelRelativePath, ".labels.txt")); + } + + public RedNetPrediction Predict(Image image) + { + using var resized = image.Clone(ctx => ctx.Resize(_inputWidth, _inputHeight)); + var tensorInput = BuildInputTensor(resized); + using var results = _session.Run([ + NamedOnnxValue.CreateFromTensor(_inputName, tensorInput) + ]); + + var logits = results.First().AsEnumerable().ToArray(); + if (logits.Length == 0) + { + throw new InvalidDataException("ONNX 模型输出为空"); + } + + var probabilities = Softmax(logits); + var maxIndex = 0; + var maxValue = probabilities[0]; + for (var i = 1; i < probabilities.Length; i++) + { + if (probabilities[i] <= maxValue) continue; + maxValue = probabilities[i]; + maxIndex = i; + } + + var label = maxIndex >= 0 && _labels is not null && maxIndex < _labels.Length + ? _labels[maxIndex] + : $"Class_{maxIndex}"; + return new RedNetPrediction(maxIndex, label, maxValue); + } + + private DenseTensor BuildInputTensor(Image image) + { + var tensor = new DenseTensor([1, 3, _inputHeight, _inputWidth]); + for (var y = 0; y < _inputHeight; y++) + { + for (var x = 0; x < _inputWidth; x++) + { + var pixel = image[x, y]; + var r = pixel.R / 255f; + var g = pixel.G / 255f; + var b = pixel.B / 255f; + + tensor[0, 0, y, x] = (r - ImagenetMean[0]) / ImagenetStd[0]; + tensor[0, 1, y, x] = (g - ImagenetMean[1]) / ImagenetStd[1]; + tensor[0, 2, y, x] = (b - ImagenetMean[2]) / ImagenetStd[2]; + } + } + + return tensor; + } + + private static float[] Softmax(float[] logits) + { + var max = logits.Max(); + var result = new float[logits.Length]; + var sum = 0d; + + for (var i = 0; i < logits.Length; i++) + { + var value = Math.Exp(logits[i] - max); + result[i] = (float)value; + sum += value; + } + + if (sum <= 0) + { + return result; + } + + for (var i = 0; i < result.Length; i++) + { + result[i] = (float)(result[i] / sum); + } + + return result; + } + + private static string[]? LoadLabels(string labelRelativePath) + { + var labelAbsolutePath = Core.Config.Global.Absolute(labelRelativePath); + if (!File.Exists(labelAbsolutePath)) + { + return null; + } + + var ext = Path.GetExtension(labelAbsolutePath); + if (ext.Equals(".json", StringComparison.OrdinalIgnoreCase)) + { + var labels = JsonSerializer.Deserialize(File.ReadAllText(labelAbsolutePath)); + return labels is { Length: > 0 } ? labels : null; + } + + var lines = File.ReadAllLines(labelAbsolutePath) + .Where(t => !string.IsNullOrWhiteSpace(t)) + .Select(t => t.Trim()) + .ToArray(); + return lines.Length > 0 ? lines : null; + } + + public void Dispose() + { + _session.Dispose(); + } +} + +public readonly record struct RedNetPrediction(int ClassIndex, string ClassLabel, float Confidence); diff --git a/BetterGenshinImpact/GameTask/AutoPick/AutoPickConfig.cs b/BetterGenshinImpact/GameTask/AutoPick/AutoPickConfig.cs index 48e46014..fb533474 100644 --- a/BetterGenshinImpact/GameTask/AutoPick/AutoPickConfig.cs +++ b/BetterGenshinImpact/GameTask/AutoPick/AutoPickConfig.cs @@ -1,4 +1,4 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using System; namespace BetterGenshinImpact.GameTask.AutoPick @@ -37,6 +37,14 @@ namespace BetterGenshinImpact.GameTask.AutoPick [ObservableProperty] private string _ocrEngine = PickOcrEngineEnum.Paddle.ToString(); + /// + /// 拾取识别模式 + /// - Ocr: 通过OCR识别物品名 + /// - RedNet: 通过分类模型直接识别物品名 + /// + [ObservableProperty] + private string _recognitionMode = PickRecognitionModeEnum.Ocr.ToString(); + /// /// 急速模式 /// 无视文字识别结果,直接拾取 diff --git a/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs b/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs index 1490ce30..394d483d 100644 --- a/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs +++ b/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs @@ -1,5 +1,6 @@ using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Recognition; +using BetterGenshinImpact.Core.Recognition.ONNX; using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.Core.Recognition.ONNX.SVTR; using BetterGenshinImpact.Core.Script.Dependence.Model.TimerConfig; @@ -8,6 +9,7 @@ using BetterGenshinImpact.GameTask.AutoPick.Assets; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Service; using BetterGenshinImpact.View.Windows; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OpenCvSharp; using System; @@ -49,9 +51,14 @@ public partial class AutoPickTrigger : ITaskTrigger private HashSet _whiteList = []; private RecognitionObject _pickRo; + private readonly Lazy _pickRedNetPredictor = new(() => + App.ServiceProvider.GetRequiredService().CreateRedNetPredictor(BgiOnnxModel.BgiPickRedNet110)); // 外部配置 private AutoPickExternalConfig? _externalConfig; + + double scale = TaskContext.Instance().SystemInfo.AssetScale; + AutoPickConfig config = TaskContext.Instance().Config.AutoPickConfig; public AutoPickTrigger() { @@ -192,8 +199,7 @@ public partial class AutoPickTrigger : ITaskTrigger return; } - var scale = TaskContext.Instance().SystemInfo.AssetScale; - var config = TaskContext.Instance().Config.AutoPickConfig; + // 存在 L 键位是千星奇遇,无需拾取 using var lKeyRa = content.CaptureRectArea.Find(_autoPickAssets.LRo); @@ -204,9 +210,10 @@ public partial class AutoPickTrigger : ITaskTrigger // 识别到拾取键,开始识别物品图标 var isExcludeIcon = false; - _autoPickAssets.ChatIconRo.RegionOfInterest = new Rect( + var iconRect = new Rect( foundRectArea.X + (int)(config.ItemIconLeftOffset * scale), foundRectArea.Y, (int)((config.ItemTextLeftOffset - config.ItemIconLeftOffset) * scale), foundRectArea.Height); + _autoPickAssets.ChatIconRo.RegionOfInterest = iconRect; using var chatIconRa = content.CaptureRectArea.Find(_autoPickAssets.ChatIconRo); speedTimer.Record("识别聊天图标"); if (!chatIconRa.IsEmpty()) @@ -251,66 +258,15 @@ public partial class AutoPickTrigger : ITaskTrigger // return; //} - // 这类文字识别比较特殊,都是针对某个场景的文字识别,所以暂时未抽象到识别对象中 - // 计算出文字区域 - var textRect = new Rect(foundRectArea.X + (int)(config.ItemTextLeftOffset * scale), foundRectArea.Y, - (int)((config.ItemTextRightOffset - config.ItemTextLeftOffset) * scale), foundRectArea.Height); - if (textRect.X + textRect.Width > content.CaptureRectArea.CacheGreyMat.Width - || textRect.Y + textRect.Height > content.CaptureRectArea.CacheGreyMat.Height) - { - Debug.WriteLine("AutoPickTrigger: 文字区域 out of range"); - return; - } - - using var gradMat = new Mat(content.CaptureRectArea.CacheGreyMat, - new Rect(textRect.X, textRect.Y, textRect.Width, Math.Min(textRect.Height, 3))); - var avgGrad = gradMat.Sobel(MatType.CV_32F, 1, 0).Mean().Val0; - if (avgGrad < -3) - { - Debug.WriteLine($"AutoPickTrigger: 已在拾取中,跳过本次拾取 {avgGrad}"); - return; - } string text; - if (config.OcrEngine == nameof(PickOcrEngineEnum.Yap)) + if (config.RecognitionMode == nameof(PickRecognitionModeEnum.RedNet)) { - var textMat = new Mat(content.CaptureRectArea.CacheGreyMat, textRect); - text = TextInferenceFactory.Pick.Value.Inference(textMat); + text = RecognizePickTextByRedNet(content, iconRect); } else { - using var textMat = new Mat(content.CaptureRectArea.SrcMat, textRect); - var boundingRect = TextRectExtractor.GetTextBoundingRect(textMat); - // var boundingRect = new Rect(); // 不使用自己写的文字区域提取 - // 如果找到有效区域 - if (boundingRect.X < 20 && boundingRect.Width > 5 && boundingRect.Height > 5) - { - // 截取只包含文字的区域 - using var textOnlyMat = new Mat(textMat, new Rect(0, 0, - boundingRect.Right + 5 < textMat.Width ? boundingRect.Right + 5 : textMat.Width, textMat.Height)); - text = OcrFactory.Paddle.OcrWithoutDetector(textOnlyMat); - - // if (RuntimeHelper.IsDebug) - // { - // // 如果不等于正确文字,则保存图片 - // if (text != "烹饪") - // { - // var path = Global.Absolute("log/pick"); - // Directory.CreateDirectory(path); - // var str = $"{DateTime.Now:yyyyMMddHHmmssfff}"; - // // textMat.SaveImage(Path.Combine(path, $"pick_ocr_ori_{str}.png")); - // // 画上 boundingRect - // Cv2.Rectangle(textMat, boundingRect, new Scalar(0, 0, 255), 1); - // textMat.SaveImage(Path.Combine(path, $"pick_ocr_rect_{str}.png")); - // bin.SaveImage(Path.Combine(path, $"bin_{str}.png")); - // } - // } - } - else - { - Debug.WriteLine("-- 无法识别到有效文字区域,尝试直接OCR DET"); - text = OcrFactory.Paddle.Ocr(textMat); - } + text = RecognizePickTextByOcr(content, foundRectArea, config.OcrEngine); } speedTimer.Record("文字识别"); @@ -370,6 +326,87 @@ public partial class AutoPickTrigger : ITaskTrigger speedTimer.DebugPrint(); } + private string RecognizePickTextByRedNet(CaptureContent content, Rect iconRect) + { + try + { + using var imageRegion = content.CaptureRectArea.DeriveCrop(iconRect); + var prediction = _pickRedNetPredictor.Value.Predict(imageRegion.CacheImage); + Debug.WriteLine($"AutoPickTrigger: RedNet预测结果 {prediction.ClassLabel} 置信度 {prediction.Confidence}"); + if (prediction.Confidence < 0.47) + { + return string.Empty; + } + + return prediction.ClassLabel; + } + catch (Exception e) + { + _logger.LogWarning(e, "AutoPick RedNet推理失败,回退OCR"); + return string.Empty; + } + } + + private string RecognizePickTextByOcr(CaptureContent content, Region foundRectArea, string ocrEngine) + { + // 这类文字识别比较特殊,都是针对某个场景的文字识别,所以暂时未抽象到识别对象中 + // 计算出文字区域 + var textRect = new Rect(foundRectArea.X + (int)(config.ItemTextLeftOffset * scale), foundRectArea.Y, + (int)((config.ItemTextRightOffset - config.ItemTextLeftOffset) * scale), foundRectArea.Height); + if (textRect.X + textRect.Width > content.CaptureRectArea.CacheGreyMat.Width + || textRect.Y + textRect.Height > content.CaptureRectArea.CacheGreyMat.Height) + { + Debug.WriteLine("AutoPickTrigger: 文字区域 out of range"); + return string.Empty; + } + + using var gradMat = new Mat(content.CaptureRectArea.CacheGreyMat, + new Rect(textRect.X, textRect.Y, textRect.Width, Math.Min(textRect.Height, 3))); + var avgGrad = gradMat.Sobel(MatType.CV_32F, 1, 0).Mean().Val0; + if (avgGrad < -3) + { + Debug.WriteLine($"AutoPickTrigger: 已在拾取中,跳过本次拾取 {avgGrad}"); + return string.Empty; + } + + if (ocrEngine == nameof(PickOcrEngineEnum.Yap)) + { + using var textMat = new Mat(content.CaptureRectArea.CacheGreyMat, textRect); + return TextInferenceFactory.Pick.Value.Inference(textMat); + } + + using var paddleMat = new Mat(content.CaptureRectArea.SrcMat, textRect); + var boundingRect = TextRectExtractor.GetTextBoundingRect(paddleMat); + // var boundingRect = new Rect(); // 不使用自己写的文字区域提取 + // 如果找到有效区域 + if (boundingRect.X < 20 && boundingRect.Width > 5 && boundingRect.Height > 5) + { + // 截取只包含文字的区域 + using var textOnlyMat = new Mat(paddleMat, new Rect(0, 0, + boundingRect.Right + 5 < paddleMat.Width ? boundingRect.Right + 5 : paddleMat.Width, paddleMat.Height)); + return OcrFactory.Paddle.OcrWithoutDetector(textOnlyMat); + + // if (RuntimeHelper.IsDebug) + // { + // // 如果不等于正确文字,则保存图片 + // if (text != "烹饪") + // { + // var path = Global.Absolute("log/pick"); + // Directory.CreateDirectory(path); + // var str = $"{DateTime.Now:yyyyMMddHHmmssfff}"; + // // textMat.SaveImage(Path.Combine(path, $"pick_ocr_ori_{str}.png")); + // // 画上 boundingRect + // Cv2.Rectangle(textMat, boundingRect, new Scalar(0, 0, 255), 1); + // textMat.SaveImage(Path.Combine(path, $"pick_ocr_rect_{str}.png")); + // bin.SaveImage(Path.Combine(path, $"bin_{str}.png")); + // } + // } + } + + Debug.WriteLine("-- 无法识别到有效文字区域,尝试直接OCR DET"); + return OcrFactory.Paddle.Ocr(paddleMat); + } + private bool DoNotPick(string text) { // 唯一一个动态拾取项,特殊处理,不拾取 @@ -576,4 +613,4 @@ public partial class AutoPickTrigger : ITaskTrigger return cleanedSpan.ToString(); } -} \ No newline at end of file +} diff --git a/BetterGenshinImpact/GameTask/AutoPick/PickOcrEngineEnum.cs b/BetterGenshinImpact/GameTask/AutoPick/PickOcrEngineEnum.cs index 2447563f..fc747485 100644 --- a/BetterGenshinImpact/GameTask/AutoPick/PickOcrEngineEnum.cs +++ b/BetterGenshinImpact/GameTask/AutoPick/PickOcrEngineEnum.cs @@ -1,4 +1,4 @@ -namespace BetterGenshinImpact.GameTask.AutoPick; +namespace BetterGenshinImpact.GameTask.AutoPick; public enum PickOcrEngineEnum { diff --git a/BetterGenshinImpact/GameTask/AutoPick/PickRecognitionModeEnum.cs b/BetterGenshinImpact/GameTask/AutoPick/PickRecognitionModeEnum.cs new file mode 100644 index 00000000..2510ed46 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoPick/PickRecognitionModeEnum.cs @@ -0,0 +1,7 @@ +namespace BetterGenshinImpact.GameTask.AutoPick; + +public enum PickRecognitionModeEnum +{ + Ocr, + RedNet +} diff --git a/BetterGenshinImpact/View/Pages/TriggerSettingsPage.xaml b/BetterGenshinImpact/View/Pages/TriggerSettingsPage.xaml index 8e173e8d..4c4b349e 100644 --- a/BetterGenshinImpact/View/Pages/TriggerSettingsPage.xaml +++ b/BetterGenshinImpact/View/Pages/TriggerSettingsPage.xaml @@ -92,12 +92,39 @@ + + + + + + + + + + + + + _pickButtonNames;