-
Notifications
You must be signed in to change notification settings - Fork 2
fix(scan): recognize current inventory icons #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,9 @@ namespace RatEye.Processing | |||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||
| public class Icon : IDisposable | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| private const float OcrVerificationThreshold = 0.7f; | ||||||||||||||||||||||||||||||||
| private static readonly Regex OcrShortNameSanitizer = new(@"[^\p{L}\p{N} \-.]"); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private readonly Config _config; | ||||||||||||||||||||||||||||||||
| private readonly Bitmap _icon; | ||||||||||||||||||||||||||||||||
| private Bitmap _scaledIcon; | ||||||||||||||||||||||||||||||||
|
|
@@ -163,6 +166,7 @@ private void SatisfyState(State targetState) | |||||||||||||||||||||||||||||||
| TemplateMatch(); | ||||||||||||||||||||||||||||||||
| if (IconConfig.ScanRotatedIcons) | ||||||||||||||||||||||||||||||||
| TemplateMatch(true); | ||||||||||||||||||||||||||||||||
| VerifyLowConfidenceTemplateMatchWithOcr(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| else if (IconConfig.ScanMode == Config.Processing.Icon.ScanModes.OCR) | ||||||||||||||||||||||||||||||||
| OCR(); | ||||||||||||||||||||||||||||||||
|
|
@@ -351,6 +355,85 @@ private void OCR() | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private void VerifyLowConfidenceTemplateMatchWithOcr() | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| if (_detectionConfidence >= OcrVerificationThreshold) | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| var langCode = ProcessingConfig.Language.ToISO3Code(); | ||||||||||||||||||||||||||||||||
| var trainedDataPath = System.IO.Path.Combine(PathConfig.TrainedData, $"{langCode}.traineddata"); | ||||||||||||||||||||||||||||||||
| if (!System.IO.File.Exists(trainedDataPath)) | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
|
Comment on lines
+363
to
+366
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Duplicated trained-data path resolution/existence check. This re-derives and re-checks the trained-data path that 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Bitmap ocrIcon = _icon.Rescale(ProcessingConfig.InverseScale * 2); | ||||||||||||||||||||||||||||||||
| try | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| var titleHeight = Math.Min( | ||||||||||||||||||||||||||||||||
| ocrIcon.Height, | ||||||||||||||||||||||||||||||||
| (int)Math.Round(ProcessingConfig.BaseSlotSize * (40f / 63f)) | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| var titleLeft = Math.Min(ocrIcon.Width - 1, (int)Math.Floor(ocrIcon.Width * 0.55f)); | ||||||||||||||||||||||||||||||||
| using var title = ocrIcon.Crop(titleLeft, 0, ocrIcon.Width - titleLeft, titleHeight); | ||||||||||||||||||||||||||||||||
| using var titleMat = title.ToMat(); | ||||||||||||||||||||||||||||||||
| using var gray = | ||||||||||||||||||||||||||||||||
| titleMat.Channels() == 1 ? titleMat.Clone() : titleMat.CvtColor(ColorConversionCodes.BGR2GRAY); | ||||||||||||||||||||||||||||||||
| using var binary = gray.Threshold(110, 255, ThresholdTypes.Binary); | ||||||||||||||||||||||||||||||||
| Cv2.BitwiseNot(binary, binary); | ||||||||||||||||||||||||||||||||
| using var enlarged = binary.Resize(new OpenCvSharp.Size(), 3, 3, InterpolationFlags.Cubic); | ||||||||||||||||||||||||||||||||
| using var filteredBitmap = enlarged.ToBitmap(); | ||||||||||||||||||||||||||||||||
| using var pix = PixConverter.ToPix(filteredBitmap); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| string text; | ||||||||||||||||||||||||||||||||
| var tesseractEngine = GetTesseractEngine(); | ||||||||||||||||||||||||||||||||
| lock (tesseractEngine) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| using var result = tesseractEngine.Process(pix, PageSegMode.SingleLine); | ||||||||||||||||||||||||||||||||
| text = result.GetText(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| var slotSize = IconSlotSize(); | ||||||||||||||||||||||||||||||||
| var items = _config.RatStashDB.GetItems(item => | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| var size = new Vector2(item.GetSlotSize()); | ||||||||||||||||||||||||||||||||
| return size == slotSize || size == slotSize.Flipped; | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| var verifiedItem = FindUniqueExactShortName(items, text); | ||||||||||||||||||||||||||||||||
| if (verifiedItem == null) | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| _item = verifiedItem; | ||||||||||||||||||||||||||||||||
| _itemExtraInfo = null; | ||||||||||||||||||||||||||||||||
| _detectionConfidence = 1; | ||||||||||||||||||||||||||||||||
| _rotated = new Vector2(verifiedItem.GetSlotSize()) != slotSize; | ||||||||||||||||||||||||||||||||
| Logger.LogDebug( | ||||||||||||||||||||||||||||||||
| $"Verified low-confidence template match as '{verifiedItem.ShortName}' using icon title OCR." | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+404
to
+410
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
When OCR verification swaps in a different 🐛 Proposed fix _item = verifiedItem;
_itemExtraInfo = null;
_detectionConfidence = 1;
+ _itemPosition = Vector2.Zero;
_rotated = new Vector2(verifiedItem.GetSlotSize()) != slotSize;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| finally | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| if (!ReferenceEquals(ocrIcon, _icon)) | ||||||||||||||||||||||||||||||||
| ocrIcon.Dispose(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+358
to
+417
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win No exception handling around the new OCR verification path. Unlike icon loading in 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| internal static Item FindUniqueExactShortName(IEnumerable<Item> items, string ocrText) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| var normalizedText = NormalizeOcrShortName(ocrText); | ||||||||||||||||||||||||||||||||
| if (string.IsNullOrWhiteSpace(normalizedText)) | ||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| var matches = items.Where(item => NormalizeOcrShortName(item.ShortName) == normalizedText).Take(2).ToList(); | ||||||||||||||||||||||||||||||||
| return matches.Count == 1 ? matches[0] : null; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| internal static string NormalizeOcrShortName(string value) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| if (string.IsNullOrWhiteSpace(value)) | ||||||||||||||||||||||||||||||||
| return ""; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return OcrShortNameSanitizer.Replace(value.CyrillicToLatin().Trim(), "").Trim().ToLowerInvariant(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||
| /// Set the item to one, best matching the scanned title | ||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ public class Inventory : IDisposable | |
| { | ||
| private readonly Config _config; | ||
| private readonly Mat _image; | ||
| private Mat _normalGridMask; | ||
| private Mat _grid; | ||
| private Mat _vertGrid; | ||
| private List<Rect> _boundingBoxes = new(); | ||
|
|
@@ -113,6 +114,7 @@ private void DetectInventoryGridNormal() | |
| var maxGridScalar = new Scalar(maxHue, maxSaturation, maxValue); | ||
| using var hsv = _image.CvtColor(ColorConversionCodes.BGR2HSV_FULL); | ||
| using var colorFilter = hsv.InRange(minGridScalar, maxGridScalar); | ||
| _normalGridMask = colorFilter.Clone(); | ||
|
|
||
| Logger.LogDebugMat(colorFilter, "inventory/colorFilter"); | ||
|
|
||
|
|
@@ -217,6 +219,8 @@ private void ParseInventoryGridNormal() | |
| } | ||
| } | ||
|
|
||
| AddContourIconsFromNormalGrid(); | ||
|
|
||
| // Quarter of the normal sized slot | ||
| var overlapThreshold = scaledSlotSize / 2; | ||
|
|
||
|
|
@@ -251,6 +255,56 @@ private void ParseInventoryGridNormal() | |
| } | ||
| } | ||
|
|
||
| private void AddContourIconsFromNormalGrid() | ||
| { | ||
| if (_normalGridMask == null || _normalGridMask.Empty()) | ||
| return; | ||
|
|
||
| using var contourSource = _normalGridMask.Clone(); | ||
| var contours = contourSource.FindContoursAsArray( | ||
| RetrievalModes.List, | ||
| ContourApproximationModes.ApproxSimple | ||
| ); | ||
|
Comment on lines
+264
to
+267
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In OpenCvSharp, using the RetrievalModes.External mode with the FindContoursAsArray method retrieves only the "extreme outer" contours of shapes in a binary image [1][2][3]. When applied to a closed shape containing a hole, this mode will ignore the inner contour (the hole) entirely [4]. Only the outermost boundary of the object will be detected and returned as a contour [1][5][6]. This behavior is consistent across all implementations of OpenCV's findContours function because RetrievalModes.External is specifically designed to ignore nested structures and hierarchy [7][6][8]. The function also sets the hierarchy information for the detected outer contour to indicate it has no parent or child relationships (hierarchy[i][2] = hierarchy[i][7] = -1) [1][7][9]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the relevant file and nearby logic.
FILE="src/ScanEngine/Processing/Inventory.cs"
echo "== Line count =="
wc -l "$FILE"
echo
echo "== Relevant range around contour handling =="
sed -n '180,320p' "$FILE"
echo
echo "== Search for contour / overlap / slot alignment helpers in the file =="
rg -n "FindContoursAsArray|RetrievalModes|IsSlotAlignedDimension|overlap|prun|contour|Bitmap|Icon" "$FILE"Repository: tarkovtracker-org/RatScanner Length of output: 8952 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/ScanEngine/Processing/Inventory.cs"
wc -l "$FILE"
sed -n '180,320p' "$FILE"
rg -n "FindContoursAsArray|RetrievalModes|IsSlotAlignedDimension|overlap|prun|contour|Bitmap|Icon" "$FILE"Repository: tarkovtracker-org/RatScanner Length of output: 8816 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/ScanEngine/Processing/Inventory.cs"
echo "== line count =="
wc -l "$FILE"
echo "== relevant range =="
sed -n '180,320p' "$FILE"
echo "== matches =="
rg -n "FindContoursAsArray|RetrievalModes|IsSlotAlignedDimension|overlap|prun|contour|Bitmap|Icon" "$FILE"Repository: tarkovtracker-org/RatScanner Length of output: 8868 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/ScanEngine/Processing/Inventory.cs"
wc -l "$FILE"
sed -n '200,300p' "$FILE"
rg -n "FindContoursAsArray|RetrievalModes|IsSlotAlignedDimension|overlap|prun|contour|Bitmap|Icon" "$FILE"Repository: tarkovtracker-org/RatScanner Length of output: 6840 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Trying to inspect repository files..."
git ls-files | rg '^src/ScanEngine/Processing/Inventory\.cs$|^src/ScanEngine/Processing/'Repository: tarkovtracker-org/RatScanner Length of output: 362 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/ScanEngine/Processing/Inventory.cs"
echo "== _normalGridMask references =="
rg -n "_normalGridMask|normalGridMask|FindContoursAsArray|RetrievalModes" "$FILE"
echo
echo "== section around mask construction / assignment =="
sed -n '80,180p' "$FILE"
echo
echo "== section around any other contour handling =="
sed -n '320,430p' "$FILE"Repository: tarkovtracker-org/RatScanner Length of output: 10135 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="src/ScanEngine/Processing/Inventory.cs"
echo "== lines 80-180 =="
sed -n '80,180p' "$FILE"
echo
echo "== lines 430-620 =="
sed -n '430,620p' "$FILE"Repository: tarkovtracker-org/RatScanner Length of output: 11580 Switch this contour retrieval to 🤖 Prompt for AI Agents |
||
| var scaledSlotSize = (int)ProcessingConfig.ScaledSlotSize; | ||
| var tolerance = Math.Max(3, (int)Math.Ceiling(scaledSlotSize * 0.08)); | ||
| var imageBounds = new Rect(0, 0, _image.Width, _image.Height); | ||
|
|
||
| using var image = _image.ToBitmap(); | ||
| foreach (var contour in contours) | ||
| { | ||
| var rect = Cv2.BoundingRect(contour); | ||
| if ( | ||
| !IsSlotAlignedDimension(rect.Width, scaledSlotSize, tolerance) | ||
| || !IsSlotAlignedDimension(rect.Height, scaledSlotSize, tolerance) | ||
| ) | ||
| continue; | ||
|
|
||
| var scaledSlotSizeVec = new Vector2(scaledSlotSize, scaledSlotSize); | ||
| var topLeft = new Vector2(rect.Location) - scaledSlotSizeVec / 8; | ||
| var size = new Vector2(rect.Size) + scaledSlotSizeVec / 4; | ||
| var paddedRect = new Rect(topLeft, size).Intersect(imageBounds); | ||
| if (paddedRect.Width <= 0 || paddedRect.Height <= 0) | ||
| continue; | ||
|
|
||
| topLeft = new Vector2(paddedRect.Location); | ||
| size = new Vector2(paddedRect.Size); | ||
| if (_icons.Any(icon => icon.Position == topLeft && icon.Size == size)) | ||
| continue; | ||
|
|
||
| var iconImage = image.Crop(topLeft.X, topLeft.Y, size.X, size.Y); | ||
| _icons.Add(new Icon(iconImage, topLeft, size, _config)); | ||
| } | ||
| } | ||
|
Comment on lines
+258
to
+297
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Add debug logging consistent with sibling detection methods. Every other detection path in this class ( 🤖 Prompt for AI AgentsSource: Coding guidelines 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Extract shared padding logic instead of duplicating The padding computation ( ♻️ Proposed extraction+ private Rect PadIconRect(Rect rect, int scaledSlotSize)
+ {
+ var scaledSlotSizeVec = new Vector2(scaledSlotSize, scaledSlotSize);
+ var topLeft = new Vector2(rect.Location) - scaledSlotSizeVec / 8;
+ var size = new Vector2(rect.Size) + scaledSlotSizeVec / 4;
+ return new Rect(topLeft, size);
+ }Then call it from both 🤖 Prompt for AI Agents |
||
|
|
||
| private static bool IsSlotAlignedDimension(int pixels, int slotSize, int tolerance) | ||
| { | ||
| if (pixels < slotSize - tolerance) | ||
| return false; | ||
|
|
||
| var slots = Math.Max(1, (int)Math.Round(pixels / (double)slotSize)); | ||
| return Math.Abs(pixels - slots * slotSize) <= tolerance; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates the stride-aware fast indexer used by the grid hot path. | ||
| /// </summary> | ||
|
|
@@ -550,6 +604,7 @@ public void Dispose() | |
| foreach (Icon icon in _icons) | ||
| icon.Dispose(); | ||
| _image.Dispose(); | ||
| _normalGridMask?.Dispose(); | ||
| _grid?.Dispose(); | ||
| _vertGrid?.Dispose(); | ||
| _disposed = true; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Two divergent OCR sanitizer regexes.
OcrShortNameSanitizer([^\p{L}\p{N} \-.], Unicode-aware) duplicates the intent of the existing ASCII-only regex inOCR()(line 345,"[^a-zA-Z0-9 -\\."]). Having two slightly different sanitization rules for OCR text risks inconsistent normalization between the full-OCR path and this new verification path. Consider reusing one shared sanitizer.🤖 Prompt for AI Agents