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