From af707f304c8ffdd0e2d7a31a12f3592fe64f2928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=89=E9=B8=AD=E8=9B=8B?= Date: Wed, 17 Sep 2025 01:46:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=87=AA=E5=8A=A8=E6=8B=BE?= =?UTF-8?q?=E5=8F=96=E7=9A=84=E9=A2=84=E5=A4=84=E7=90=86=E7=8E=AF=E8=8A=82?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E6=9C=80=E7=BB=88=E6=96=87=E5=AD=97?= =?UTF-8?q?=E5=B1=95=E7=8E=B0=EF=BC=8C=E4=BD=BF=E8=87=AA=E5=8A=A8=E6=8B=BE?= =?UTF-8?q?=E5=8F=96OCR=E8=AF=86=E5=88=AB=E6=9B=B4=E5=8A=A0=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=20(#2211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GameTask/AutoPick/AutoPickTrigger.cs | 150 ++++++++++++++++-- .../GameTask/AutoPick/TextRectExtractor.cs | 79 +++++++++ .../Cv/ThresholdWindow.cs | 81 ++++++++++ 3 files changed, 295 insertions(+), 15 deletions(-) create mode 100644 BetterGenshinImpact/GameTask/AutoPick/TextRectExtractor.cs create mode 100644 Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs diff --git a/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs b/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs index 7ba28385..d0247fff 100644 --- a/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs +++ b/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs @@ -12,6 +12,7 @@ using OpenCvSharp; using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; @@ -245,17 +246,34 @@ public partial class AutoPickTrigger : ITaskTrigger else { var textMat = new Mat(content.CaptureRectArea.SrcMat, textRect); - var boundingRect = GetWhiteTextBoundingRect(textMat); + var boundingRect = TextRectExtractor.GetTextBoundingRect(textMat, out var bin); // 如果找到有效区域 - if (boundingRect.Width > 5 && boundingRect.Height > 5) + if (boundingRect.X <20 && boundingRect.Width > 5 && boundingRect.Height > 5) { // 截取只包含文字的区域 var textOnlyMat = new Mat(textMat, new Rect(0, 0, - boundingRect.Right + 3 < textMat.Width ? boundingRect.Right + 3 : textMat.Width, textMat.Height)); + 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); } } @@ -263,6 +281,9 @@ public partial class AutoPickTrigger : ITaskTrigger speedTimer.Record("文字识别"); if (!string.IsNullOrEmpty(text)) { + // 处理OCR识别结果,清理无效字符并确保引号配对 + text = ProcessOcrText(text); + // 唯一一个动态拾取项,特殊处理,不拾取 if (text.Contains("长时间")) { @@ -277,19 +298,12 @@ public partial class AutoPickTrigger : ITaskTrigger } // 单个字符不拾取 - var simpleText = PunctuationAndSpacesRegex().Replace(text, ""); - if (simpleText.Length <= 1) - { - return; - } - - // 纯英文不拾取 - if (StringUtils.IsPureEnglish(text)) + if (text.Length <= 1) { return; } - if (config.WhiteListEnabled && (_whiteList.Contains(text) || _whiteList.Contains(simpleText))) + if (config.WhiteListEnabled && _whiteList.Contains(text)) { LogPick(content, text); Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk); @@ -304,7 +318,7 @@ public partial class AutoPickTrigger : ITaskTrigger return; } - if (config.BlackListEnabled && (_blackList.Contains(text) || _blackList.Contains(simpleText))) + if (config.BlackListEnabled && _blackList.Contains(text)) { return; } @@ -372,8 +386,114 @@ public partial class AutoPickTrigger : ITaskTrigger _prevClickFrameIndex = content.FrameIndex; } - [GeneratedRegex(@"^[\p{P} ]+|[\p{P} ]+$")] - private static partial Regex PunctuationAndSpacesRegex(); + /// + /// 高性能处理OCR识别的文字结果 + /// 1. 替换【、[ 为「,替换】、] 为」 + /// 2. 清理左边非「字符和中文的字符 + /// 3. 清理右边非」字符和中文的字符 + /// 4. 确保引号配对:有「必有」,有」必有「 + /// + /// OCR识别的原始文字 + /// 处理后的文字 + private static string ProcessOcrText(string text) + { + if (string.IsNullOrEmpty(text)) + return text; + + // 0. 首先替换相似的括号字符并删除换行符、空格,使用Span进行原地替换以获得最佳性能 + Span chars = stackalloc char[text.Length]; + text.AsSpan().CopyTo(chars); + + int writeIndex = 0; + bool hasChanges = false; + + for (int i = 0; i < chars.Length; i++) + { + char c = chars[i]; + + // 跳过换行符、回车符、空格、制表符等空白字符 + if (char.IsWhiteSpace(c)) + { + hasChanges = true; + continue; + } + + // 替换括号字符 + if (c == '【' || c == '[') + { + chars[writeIndex++] = '「'; + hasChanges = true; + } + else if (c == '】' || c == ']') + { + chars[writeIndex++] = '」'; + hasChanges = true; + } + else + { + chars[writeIndex++] = c; + } + } + + // 如果有变化,使用处理后的字符;否则使用原字符串的Span + ReadOnlySpan span = hasChanges ? chars.Slice(0, writeIndex) : text.AsSpan(); + int start = 0; + int end = span.Length - 1; + + // 1. 从左边开始,删除非「字符和中文的字符 + while (start <= end) + { + char c = span[start]; + if (c == '「' || (c >= 0x4E00 && c <= 0x9FFF)) // 「字符或中文字符 + break; + start++; + } + + // 2. 从右边开始,删除非」字符和中文的字符 + while (end >= start) + { + char c = span[end]; + if (c == '」' || c == '!' || (c >= 0x4E00 && c <= 0x9FFF)) // 」字符或中文字符 + break; + end--; + } + + // 如果所有字符都被删除了 + if (start > end) + return string.Empty; + + // 获取清理后的文字 + var cleanedSpan = span.Slice(start, end - start + 1); + + // 3. 检查并补充引号配对 + bool hasLeftQuote = false; + bool hasRightQuote = false; + + // 快速扫描是否存在引号 + for (int i = 0; i < cleanedSpan.Length; i++) + { + if (cleanedSpan[i] == '「') + hasLeftQuote = true; + else if (cleanedSpan[i] == '」') + hasRightQuote = true; + } + + // 根据引号配对规则补充 + if (hasLeftQuote && !hasRightQuote) + { + // 有「但没有」,在末尾补充」 + Debug.WriteLine("补充缺失的右引号"); + return string.Concat(cleanedSpan, "」"); + } + else if (hasRightQuote && !hasLeftQuote) + { + // 有」但没有「,在开头补充「 + Debug.WriteLine("补充缺失的左引号"); + return string.Concat("「", cleanedSpan); + } + + return cleanedSpan.ToString(); + } } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoPick/TextRectExtractor.cs b/BetterGenshinImpact/GameTask/AutoPick/TextRectExtractor.cs new file mode 100644 index 00000000..ac15db89 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoPick/TextRectExtractor.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; + +namespace BetterGenshinImpact.GameTask.AutoPick; + +using OpenCvSharp; + +public static class TextRectExtractor +{ + /// + /// 从图片中提取文字范围(假定文字从最左边贴边开始,向右连续) + /// 结果矩形固定 x=0,y=0,h=原图高度,只计算连续文字宽度。 + /// + public static Rect GetTextBoundingRect(Mat textMat, out Mat bin) + { + // 转换为灰度图 + Mat gray = new Mat(); + if (textMat.Channels() == 3) + { + Cv2.CvtColor(textMat, gray, ColorConversionCodes.BGR2GRAY); + } + else + { + gray = textMat.Clone(); + } + + // 使用阈值160进行二值化处理 + bin = new Mat(); + Cv2.Threshold(gray, bin, 160, 255, ThresholdTypes.Binary); + + // 形态学操作:先腐蚀后膨胀,去除噪点并保持文字完整 + Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3)); + Cv2.Erode(bin, bin, kernel, iterations: 1); + Cv2.Dilate(bin, bin, kernel, iterations: 2); + kernel.Dispose(); + gray.Dispose(); + return ProjectionRect(textMat, bin); + } + + private static Rect ProjectionRect(Mat textMat, Mat bin) + { + // 投影:对行做 ReduceSum,得到 1 x width 的列和 + using var projection = new Mat(); + Cv2.Reduce(bin, projection, 0, ReduceTypes.Sum, MatType.CV_32S); + int width = projection.Cols; + projection.GetArray(out int[] colSums); + + int maxGap = 30; // 允许的最大连续空列数 + int gapCount = 0; + int lastNonEmpty = -1; + + for (int x = 0; x < width; x++) + { + bool hasInk = colSums[x] > 0; + if (hasInk) + { + lastNonEmpty = x; + gapCount = 0; + } + else + { + gapCount++; + if (gapCount > maxGap) + { + break; + } + } + } + + if (lastNonEmpty == -1) + { + // 没有检测到文字 + return new Rect(); + } + + Rect boundingRect = new Rect(0, 0, lastNonEmpty, textMat.Height); + return boundingRect; + } +} diff --git a/Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs b/Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs new file mode 100644 index 00000000..eb37d2eb --- /dev/null +++ b/Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs @@ -0,0 +1,81 @@ +using OpenCvSharp; + +namespace BetterGenshinImpact.Test.Cv; + +public class ThresholdWindow +{ + private Mat? _originalImage; + private Mat? _grayImage; + private int _currentThreshold = 160; + + + public static void Test() + { + var window = new ThresholdWindow(); + window.ShowThresholdAdjuster(@"E:\HuiTask\更好的原神\自动拾取\pick_ocr_ori_20250915011455192.png"); + } + + /// + /// 对指定图片进行二值化阈值拉条调整 + /// + /// 图片路径 + public void ShowThresholdAdjuster(string imagePath) + { + // 加载原始图像 + _originalImage = Cv2.ImRead(imagePath); + if (_originalImage.Empty()) + { + throw new ArgumentException("无法加载图像文件"); + } + + // 转换为灰度图像 + _grayImage = new Mat(); + Cv2.CvtColor(_originalImage, _grayImage, ColorConversionCodes.BGR2GRAY); + + // 创建窗口 + const string windowName = "Threshold Adjuster"; + const string trackbarName = "Threshold"; + + Cv2.NamedWindow(windowName, WindowFlags.AutoSize); + + // 创建拉条,范围0-255 + Cv2.CreateTrackbar(trackbarName, windowName, ref _currentThreshold, 255, OnThresholdChanged); + + // 初始显示 + UpdateThresholdImage(windowName); + + // 等待用户按键 + Console.WriteLine("按任意键关闭窗口..."); + Cv2.WaitKey(0); + + // 清理资源 + Cv2.DestroyAllWindows(); + _originalImage?.Dispose(); + _grayImage?.Dispose(); + } + + /// + /// 阈值变化回调函数 + /// + /// 阈值 + /// 用户数据指针(未使用) + private void OnThresholdChanged(int value, IntPtr userdata) + { + _currentThreshold = value; + UpdateThresholdImage("Threshold Adjuster"); + } + + /// + /// 更新二值化图像显示 + /// + /// 窗口名称 + private void UpdateThresholdImage(string windowName) + { + if (_grayImage == null) return; + + using var thresholdImage = new Mat(); + Cv2.Threshold(_grayImage, thresholdImage, _currentThreshold, 255, ThresholdTypes.Binary); + + Cv2.ImShow(windowName, thresholdImage); + } +} \ No newline at end of file