diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs index 4f25458c..43950814 100644 --- a/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs @@ -87,4 +87,24 @@ public static class CommonExtension { return list.ConvertAll(ToPoint2d); } + + /// + /// 将矩形钳位到指定尺寸范围内(交集语义),防止 OpenCV ROI 越界 + /// + public static Rect ClampTo(this Rect rect, int maxWidth, int maxHeight) + { + int x1 = Math.Clamp(rect.X, 0, maxWidth); + int y1 = Math.Clamp(rect.Y, 0, maxHeight); + int x2 = Math.Clamp(rect.X + rect.Width, 0, maxWidth); + int y2 = Math.Clamp(rect.Y + rect.Height, 0, maxHeight); + return new Rect(x1, y1, x2 - x1, y2 - y1); + } + + /// + /// 将矩形钳位到 Mat 范围内(交集语义),防止 OpenCV ROI 越界 + /// + public static Rect ClampTo(this Rect rect, Mat mat) + { + return rect.ClampTo(mat.Cols, mat.Rows); + } } diff --git a/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTask.cs b/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTask.cs index f90c8205..1397c2f3 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTask.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTask.cs @@ -391,6 +391,9 @@ namespace BetterGenshinImpact.GameTask.AutoFishing clickWhiteConfirmButtonWaitEndTime < timeProvider.GetLocalNow()) && Bv.ClickWhiteConfirmButton(imageRegion)) { + // 截取鱼饵图标区域(正方形,宽高均取 6.5% 的屏幕宽度) + // 最后一个参数有意用 Width 而非 Height,目的是保持正方形裁剪 + // 经验算在 16:9 常见分辨率(720p/1080p/1440p)下 Y+H 不会超出图像高度,暂不加钳位 using Mat subMat = imageRegion.SrcMat.SubMat(new Rect((int)(0.824 * imageRegion.Width), (int)(0.669 * imageRegion.Height), (int)(0.065 * imageRegion.Width), (int)(0.065 * imageRegion.Width))); using Mat resized = subMat.Resize(new Size(125, 125)); (string predName, _) = GridIconsAccuracyTestTask.Infer(resized, this.session, this.prototypes); diff --git a/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs b/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs index 1e3a5d68..d6d32e60 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs @@ -857,8 +857,13 @@ namespace BetterGenshinImpact.GameTask.AutoFishing } int hExtra = _cur.Height, vExtra = _cur.Height / 4; - blackboard.fishBoxRect = new Rect(_cur.X - hExtra, _cur.Y - vExtra, - (topMat.Width / 2 - _cur.X) * 2 + hExtra * 2, _cur.Height + vExtra * 2); + { + int rx = _cur.X - hExtra; + int ry = _cur.Y - vExtra; + int rw = (topMat.Width / 2 - _cur.X) * 2 + hExtra * 2; + int rh = _cur.Height + vExtra * 2; + blackboard.fishBoxRect = new Rect(rx, ry, rw, rh).ClampTo(imageRegion.SrcMat); + } using var boxRa = imageRegion.Derive(blackboard.fishBoxRect); boxRa.DrawSelf("FishBox", System.Drawing.Pens.LightPink); logger.LogInformation(" 识别到钓鱼框"); diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/GeniusInvokationControl.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/GeniusInvokationControl.cs index 4cc0cde3..684cccdf 100644 --- a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/GeniusInvokationControl.cs +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/GeniusInvokationControl.cs @@ -998,11 +998,10 @@ public class GeniusInvokationControl public void AppendCharacterStatus(Character character, Mat greyMat, int hp = -2) { - // 截取出战角色区域扩展 - using var characterMat = new Mat(greyMat, new Rect(character.Area.X, - character.Area.Y, - character.Area.Width + 40, - character.Area.Height + 10)); + // 截取出战角色区域扩展,钳位到图像边界防止越界 + var charRect = new Rect(character.Area.X, character.Area.Y, + character.Area.Width + 40, character.Area.Height + 10).ClampTo(greyMat); + using var characterMat = new Mat(greyMat, charRect); // 识别角色异常状态 var pCharacterStatusFreeze = MatchTemplateHelper.MatchTemplate(characterMat, _assets.CharacterStatusFreezeMat, TemplateMatchModes.CCoeffNormed); @@ -1144,9 +1143,13 @@ public class GeniusInvokationControl } else { - hpMat = new Mat(imageRegion.SrcMat, new Rect(cardRect.X + _config.CharacterCardExtendHpRect.X, + // 出战角色 HP 区域向上偏移,钳位到图像边界防止越界 + var activeHpRect = new Rect( + cardRect.X + _config.CharacterCardExtendHpRect.X, cardRect.Y + _config.CharacterCardExtendHpRect.Y - _config.ActiveCharacterCardSpace, - _config.CharacterCardExtendHpRect.Width, _config.CharacterCardExtendHpRect.Height)); + _config.CharacterCardExtendHpRect.Width, + _config.CharacterCardExtendHpRect.Height).ClampTo(imageRegion.SrcMat); + hpMat = new Mat(imageRegion.SrcMat, activeHpRect); text = OcrFactory.Paddle.Ocr(hpMat); //Cv2.ImWrite($"log\\hp_active_{i}.jpg", hpMat); Debug.WriteLine($"角色{i}出战HP位置识别结果{text}"); diff --git a/BetterGenshinImpact/GameTask/AutoLeyLineOutcrop/AutoLeyLineOutcropTask.cs b/BetterGenshinImpact/GameTask/AutoLeyLineOutcrop/AutoLeyLineOutcropTask.cs index 9315831c..3fe2f271 100644 --- a/BetterGenshinImpact/GameTask/AutoLeyLineOutcrop/AutoLeyLineOutcropTask.cs +++ b/BetterGenshinImpact/GameTask/AutoLeyLineOutcrop/AutoLeyLineOutcropTask.cs @@ -870,7 +870,7 @@ public class AutoLeyLineOutcropTask : ISoloTask return capture.Find(ro); } - var clamped = ClampRect(roi, capture.Width, capture.Height); + var clamped = roi.ClampTo(capture.Width, capture.Height); if (clamped.Width <= 0 || clamped.Height <= 0) { return new Region(); @@ -886,16 +886,6 @@ public class AutoLeyLineOutcropTask : ISoloTask return capture.Find(cloned); } - private static Rect ClampRect(Rect roi, int maxWidth, int maxHeight) - { - // Clamp ROI to avoid OpenCV exceptions when the rectangle is out of bounds. - var x = Math.Clamp(roi.X, 0, Math.Max(0, maxWidth - 1)); - var y = Math.Clamp(roi.Y, 0, Math.Max(0, maxHeight - 1)); - var w = Math.Clamp(roi.Width, 0, Math.Max(0, maxWidth - x)); - var h = Math.Clamp(roi.Height, 0, Math.Max(0, maxHeight - y)); - return new Rect(x, y, w, h); - } - private async Task AutoFight(int timeoutSeconds) { var fightCts = CancellationTokenSource.CreateLinkedTokenSource(_ct); diff --git a/BetterGenshinImpact/GameTask/GetGridIcons/GetGridIconsTask.cs b/BetterGenshinImpact/GameTask/GetGridIcons/GetGridIconsTask.cs index 3979c8bb..1f847ff5 100644 --- a/BetterGenshinImpact/GameTask/GetGridIcons/GetGridIconsTask.cs +++ b/BetterGenshinImpact/GameTask/GetGridIcons/GetGridIconsTask.cs @@ -1,4 +1,5 @@ using BetterGenshinImpact.Core.Recognition.OCR; +using BetterGenshinImpact.Core.Recognition.OpenCv; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.GameTask.Common; @@ -243,7 +244,11 @@ public class GetGridIconsTask : ISoloTask double scale = (systemInfo ?? TaskContext.Instance().SystemInfo).AssetScale; double width = 60; double height = 60; // 宽高缩放似乎不一致,似乎在2.05:2.15之间,但不知道怎么测定 - Rect iconRect = new Rect((int)(itemRegion.Width / 2 - 237 * scale - width / 2), (int)(itemRegion.Height / 2 - height / 2), (int)width, (int)height); + // 低分辨率下 237 * scale 的偏移量可能大于 itemRegion 中心位置,导致 X 为负,加保护 + Rect iconRect = new Rect( + (int)(itemRegion.Width / 2 - 237 * scale - width / 2), + (int)(itemRegion.Height / 2 - height / 2), + (int)width, (int)height).ClampTo(itemRegion.SrcMat); using Mat crop = itemRegion.SrcMat.SubMat(iconRect); return crop.Resize(new Size(125, 125)); } diff --git a/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs b/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs index 405766ed..dc2eb98c 100644 --- a/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs +++ b/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs @@ -69,7 +69,12 @@ public class ImageRegion : Region /// public ImageRegion DeriveCrop(int x, int y, int w, int h) { - return new ImageRegion(new Mat(SrcMat, new Rect(x, y, w, h)), x, y, this, new TranslationConverter(x, y)); + var rect = new Rect(x, y, w, h).ClampTo(SrcMat); + if (rect.Width <= 0 || rect.Height <= 0) + { + throw new ArgumentOutOfRangeException(nameof(rect), $"DeriveCrop 裁剪区域无效: ({x},{y},{w},{h}),图像大小: {SrcMat.Cols}x{SrcMat.Rows}"); + } + return new ImageRegion(new Mat(SrcMat, rect), rect.X, rect.Y, this, new TranslationConverter(rect.X, rect.Y)); } public ImageRegion DeriveCrop(double dx, double dy, double dw, double dh) @@ -78,7 +83,12 @@ public class ImageRegion : Region var y = (int)Math.Round(dy); var w = (int)Math.Round(dw); var h = (int)Math.Round(dh); - return new ImageRegion(new Mat(SrcMat, new Rect(x, y, w, h)), x, y, this, new TranslationConverter(x, y)); + var rect = new Rect(x, y, w, h).ClampTo(SrcMat); + if (rect.Width <= 0 || rect.Height <= 0) + { + throw new ArgumentOutOfRangeException(nameof(rect), $"DeriveCrop 裁剪区域无效: ({x},{y},{w},{h}),图像大小: {SrcMat.Cols}x{SrcMat.Rows}"); + } + return new ImageRegion(new Mat(SrcMat, rect), rect.X, rect.Y, this, new TranslationConverter(rect.X, rect.Y)); } public ImageRegion DeriveCrop(Rect rect) diff --git a/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs b/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs index abd74a22..a49adad6 100644 --- a/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs +++ b/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs @@ -339,6 +339,14 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI var result = cells.ToList(); foreach (var cell in cells.Where(c => c.IsPhantom)) { + // 幻影格子由插值生成,低分辨率下可能坐标越界,直接丢弃 + if (cell.Rect.X < 0 || cell.Rect.Y < 0 || + cell.Rect.X + cell.Rect.Width > mat.Cols || + cell.Rect.Y + cell.Rect.Height > mat.Rows) + { + result.Remove(cell); + continue; + } using Mat cellMat = mat.SubMat(cell.Rect); using Mat bottom = cellMat.GetGridBottom(); if (!IsCorrectBottomColor(bottom)) diff --git a/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/RectClampTests.cs b/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/RectClampTests.cs new file mode 100644 index 00000000..5aef9179 --- /dev/null +++ b/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/RectClampTests.cs @@ -0,0 +1,38 @@ +using BetterGenshinImpact.Core.Recognition.OpenCv; +using OpenCvSharp; + +namespace BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests; + +public class RectClampTests +{ + [Theory] + [InlineData(10, 10, 50, 50, 200, 200, 10, 10, 50, 50)] // 完全在范围内 + [InlineData(-10, 20, 100, 50, 200, 200, 0, 20, 90, 50)] // 左侧越界 + [InlineData(20, -15, 50, 100, 200, 200, 20, 0, 50, 85)] // 上方越界 + [InlineData(150, 10, 100, 50, 200, 200, 150, 10, 50, 50)] // 右侧越界 + [InlineData(10, 150, 50, 100, 200, 200, 10, 150, 50, 50)] // 下方越界 + [InlineData(-10, -20, 300, 400, 100, 100, 0, 0, 100, 100)] // 四边都越界 + [InlineData(-100, 10, 50, 50, 200, 200, 0, 10, 0, 50)] // 完全在外(左),宽为0 + [InlineData(0, 0, 10, 10, 0, 0, 0, 0, 0, 0)] // 零尺寸图像 + public void ClampTo_IntOverload_ReturnsExpected( + int x, int y, int w, int h, + int maxW, int maxH, + int ex, int ey, int ew, int eh) + { + var rect = new Rect(x, y, w, h); + var result = rect.ClampTo(maxW, maxH); + Assert.Equal(new Rect(ex, ey, ew, eh), result); + } + + [Fact] + public void ClampTo_MatOverload_MatchesIntOverload() + { + var rect = new Rect(-10, -5, 100, 80); + using var mat = new Mat(200, 200, MatType.CV_8UC3); + + var result = rect.ClampTo(mat); + + Assert.Equal(rect.ClampTo(mat.Cols, mat.Rows), result); + Assert.Equal(new Rect(0, 0, 90, 75), result); + } +}