Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/ScanEngine/IconManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,12 @@ private Dictionary<Vector2, Dictionary<string, Mat>> LoadNewIcons(
return;

var useCache = _config.ProcessingConfig.UseCache;
var sourceFile = new FileInfo(iconPath);
var cacheIdentity =
$"{iconKey}|{sourceFile.Length}|{sourceFile.LastWriteTimeUtc.Ticks}";
var cacheIconPath = Path.Combine(
_cacheDirectory,
$"{iconKey.CacheKey(configHash)}.bmp"
$"{cacheIdentity.CacheKey(configHash)}.bmp"
);
icon = useCache ? TryLoadCachedIcon(cacheIconPath) : null;
var cacheHit = icon != null;
Expand All @@ -242,6 +245,11 @@ private Dictionary<Vector2, Dictionary<string, Mat>> LoadNewIcons(
using var mat = Cv2.ImRead(iconPath, ImreadModes.Unchanged);
if (mat.Empty())
throw new InvalidDataException("The icon image is empty or unreadable.");
if (!HasVisibleIconContent(mat))
{
Logger.LogDebug("Skipping icon without visible pixels: " + iconPath);
return;
}
icon = GetIconWithBackground(mat, item);
}

Expand Down Expand Up @@ -303,6 +311,18 @@ private Dictionary<Vector2, Dictionary<string, Mat>> LoadNewIcons(
return loadedIcons;
}

private static bool HasVisibleIconContent(Mat icon)
{
if (icon.Channels() == 4)
{
using var alpha = icon.ExtractChannel(3);
return Cv2.CountNonZero(alpha) > 0;
}

using var gray = icon.Channels() == 1 ? icon.Clone() : icon.CvtColor(ColorConversionCodes.BGR2GRAY);
return Cv2.CountNonZero(gray) > 0;
}

private static bool IsRecoverableIconLoadException(Exception exception) =>
IsRecoverableFileSystemException(exception)
|| exception is ArgumentException or OpenCVException or OpenCvSharpException;
Expand Down
83 changes: 83 additions & 0 deletions src/ScanEngine/Processing/Icon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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} \-.]");

Comment on lines +19 to +21

Copy link
Copy Markdown

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 in OCR() (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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ScanEngine/Processing/Icon.cs` around lines 19 - 21, Unify OCR
sanitization by extracting the existing regex used in OCR() into the shared
OcrShortNameSanitizer symbol, then reuse it in both the full-OCR and
verification paths. Remove the divergent Unicode-aware pattern so both paths
apply identical normalization rules.

private readonly Config _config;
private readonly Bitmap _icon;
private Bitmap _scaledIcon;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

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

Duplicated trained-data path resolution/existence check.

This re-derives and re-checks the trained-data path that GetTesseractEngine() (line ~483) already builds and checks independently, using a different construction strategy (Path.Combine here vs. manual "\\" concatenation there). Functionally equivalent on Windows, but it's duplicated logic and a duplicate File.Exists syscall on every low-confidence match; consider extracting a single helper (e.g. TryGetTrainedDataPath) used by both call sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ScanEngine/Processing/Icon.cs` around lines 363 - 366, Extract the
trained-data path construction and existence check from the current
low-confidence flow and GetTesseractEngine() into a shared helper such as
TryGetTrainedDataPath. Update both call sites to reuse that helper, preserving
the existing language-code resolution and early-return behavior while
eliminating duplicate path logic and File.Exists checks.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

_itemPosition goes stale when OCR overrides the item.

When OCR verification swaps in a different verifiedItem, _itemPosition still holds the position computed for the discarded, low-confidence template match — which was for a different (likely wrong) item entirely. _rotated is correctly recomputed for the new item, but the position isn't, so ItemPosition will report a misleading location tied to the wrong candidate's match.

🐛 Proposed fix
                 _item = verifiedItem;
                 _itemExtraInfo = null;
                 _detectionConfidence = 1;
+                _itemPosition = Vector2.Zero;
                 _rotated = new Vector2(verifiedItem.GetSlotSize()) != slotSize;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_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."
);
_item = verifiedItem;
_itemExtraInfo = null;
_detectionConfidence = 1;
_itemPosition = Vector2.Zero;
_rotated = new Vector2(verifiedItem.GetSlotSize()) != slotSize;
Logger.LogDebug(
$"Verified low-confidence template match as '{verifiedItem.ShortName}' using icon title OCR."
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ScanEngine/Processing/Icon.cs` around lines 404 - 410, When OCR
verification replaces the low-confidence match in the verification block,
recompute or reset _itemPosition for the new verifiedItem so ItemPosition no
longer uses the discarded candidate’s position. Update the same assignment
section that sets _item, _itemExtraInfo, _detectionConfidence, and _rotated,
preserving the existing position calculation behavior used elsewhere for
verified items.

}
finally
{
if (!ReferenceEquals(ocrIcon, _icon))
ocrIcon.Dispose();
}
}
Comment on lines +358 to +417

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 IconManager (which wraps OpenCV/Tesseract/IO failures via IsRecoverableIconLoadException), VerifyLowConfidenceTemplateMatchWithOcr has no try/catch. Previously, TemplateMatching mode had zero runtime dependency on Tesseract; now every low-confidence match opportunistically runs OCR, so any native/Tesseract failure (corrupt traineddata, native crash, malformed pix) will propagate unguarded out of Item/DetectionConfidence/ItemPosition getters and can crash a scan that previously never touched OCR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ScanEngine/Processing/Icon.cs` around lines 358 - 417, The
VerifyLowConfidenceTemplateMatchWithOcr method must contain exception handling
around its OpenCV/Tesseract OCR operations so failures do not escape Item,
DetectionConfidence, or ItemPosition accessors. Reuse the existing
IsRecoverableIconLoadException policy and logging behavior from IconManager,
while preserving the finally block’s disposal of ocrIcon and allowing successful
OCR verification to remain unchanged.


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>
Expand Down
55 changes: 55 additions & 0 deletions src/ScanEngine/Processing/Inventory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -217,6 +219,8 @@ private void ParseInventoryGridNormal()
}
}

AddContourIconsFromNormalGrid();

// Quarter of the normal sized slot
var overlapThreshold = scaledSlotSize / 2;

Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

OpenCvSharp FindContoursAsArray RetrievalModes.External behavior for closed shape with hole

💡 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 RetrievalModes.External
RetrievalModes.List returns both sides of the closed 1px slot border, so each cell produces duplicate contour crops that are only removed later. External keeps the outer contour and avoids that extra work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ScanEngine/Processing/Inventory.cs` around lines 264 - 267, Update the
FindContoursAsArray call in the contour processing flow to use
RetrievalModes.External instead of RetrievalModes.List, while preserving the
existing ContourApproximationModes.ApproxSimple setting.

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

Copy link
Copy Markdown

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

Add debug logging consistent with sibling detection methods.

Every other detection path in this class (DetectInventoryGridNormal, ParseInventoryGridHighlighted, LocateIcon) logs intermediate masks/candidate rectangles via Logger.LogDebugMat under Config.LogDebug. This new contour-fallback path adds no equivalent visualization, making it harder to diagnose accuracy issues (e.g., over/under-detection, alignment tolerance tuning) in the field. As per coding guidelines, "Log through existing engine logger patterns."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ScanEngine/Processing/Inventory.cs` around lines 258 - 297, The
AddContourIconsFromNormalGrid method should add debug visualization consistent
with DetectInventoryGridNormal, ParseInventoryGridHighlighted, and LocateIcon.
Under Config.LogDebug, log the contour source mask and relevant candidate or
padded rectangles through the existing Logger.LogDebugMat pattern, preserving
the current detection behavior and avoiding logging when debug mode is disabled.

Source: Coding guidelines


📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract shared padding logic instead of duplicating TryAddIcon's formula.

The padding computation (topLeft -= scaledSlotSizeVec/8; size += scaledSlotSizeVec/4;) is copy-pasted verbatim from TryAddIcon (Lines 497-500). If the padding heuristic is ever tuned for one path, the other will silently diverge, causing inconsistent icon crop sizes between grid-detected and contour-detected icons.

♻️ 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 AddContourIconsFromNormalGrid and TryAddIcon.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ScanEngine/Processing/Inventory.cs` around lines 258 - 297, Extract the
shared padding calculation currently duplicated in AddContourIconsFromNormalGrid
and TryAddIcon into a reusable helper. Have both methods call that helper for
the top-left and size adjustments, preserving the existing padding behavior and
ensuring future heuristic changes apply consistently.


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>
Expand Down Expand Up @@ -550,6 +604,7 @@ public void Dispose()
foreach (Icon icon in _icons)
icon.Dispose();
_image.Dispose();
_normalGridMask?.Dispose();
_grid?.Dispose();
_vertGrid?.Dispose();
_disposed = true;
Expand Down
64 changes: 63 additions & 1 deletion tests/RatScanner.Tests/OpenCvPipelineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using RatEye;
Expand Down Expand Up @@ -140,6 +141,57 @@ public void Normal_inventory_rejects_a_slot_scale_too_small_for_safe_edge_walkin
Assert.Contains("at least two pixels", exception.Message, StringComparison.Ordinal);
}

[Theory]
[InlineData("F-1/", "f-1")]
[InlineData(" F-1[\r\n", "f-1")]
[InlineData("", "")]
public void Icon_OCR_short_name_normalization_removes_UI_noise(string source, string expected) =>
Assert.Equal(expected, RatEye.Processing.Icon.NormalizeOcrShortName(source));

[Fact]
public void Icon_OCR_short_name_verification_requires_a_unique_exact_match()
{
RatStash.Item expected = new() { Id = "f1", ShortName = "F-1" };
RatStash.Item other = new() { Id = "other", ShortName = "Other" };

Assert.Same(expected, RatEye.Processing.Icon.FindUniqueExactShortName([expected, other], "F-1/"));
Assert.Null(
RatEye.Processing.Icon.FindUniqueExactShortName(
[expected, new RatStash.Item { Id = "duplicate", ShortName = "F-1" }],
"F-1"
)
);
}

[Fact]
public void Inventory_locates_adjacent_current_ui_cells_from_one_pixel_borders()
{
using Bitmap source = new(250, 150);
using (Graphics graphics = Graphics.FromImage(source))
{
graphics.Clear(System.Drawing.Color.Black);
using Pen gridPen = new(System.Drawing.Color.FromArgb(73, 81, 84), 1);
graphics.DrawRectangle(gridPen, 11, 36, 84, 84);
graphics.DrawRectangle(gridPen, 95, 36, 84, 84);
}

Config config = new()
{
ProcessingConfig = new Config.Processing
{
Scale = 4f / 3f,
InventoryConfig = new Config.Processing.Inventory { OptimizeHighlighted = false },
},
};

using RatEyeEngine engine = new(config, RatStash.Database.FromItems([]));
using RatEye.Processing.Inventory inventory = engine.NewInventory(source);

Assert.Equal(2, inventory.Icons.Count());
Assert.NotNull(inventory.LocateIcon(new Vector2(53, 79)));
Assert.NotNull(inventory.LocateIcon(new Vector2(137, 79)));
}

[Fact]
public void Blank_inspection_is_a_low_confidence_failure_without_ocr()
{
Expand Down Expand Up @@ -245,6 +297,8 @@ public void Static_icon_template_matching_identifies_an_exact_generated_fixture(
using Mat iconMat = BitmapConverter.ToMat(iconSource);
Assert.True(Cv2.ImWrite(iconPath, iconMat));
}
using (Bitmap blankSource = new(64, 64))
blankSource.Save(Path.Combine(root, "blank.png"), System.Drawing.Imaging.ImageFormat.Png);
File.WriteAllText(Path.Combine(root, "broken.png"), "not an image");

RatStash.Item expected = new()
Expand All @@ -263,12 +317,20 @@ public void Static_icon_template_matching_identifies_an_exact_generated_fixture(
Width = 1,
Height = 1,
};
RatStash.Item blank = new()
{
Id = "blank",
Name = "Blank fixture",
ShortName = "Blank",
Width = 1,
Height = 1,
};
Config config = CreateProcessingConfig(optimizeHighlighted: false);
config.PathConfig.StaticIcons = root;
config.ProcessingConfig.IconConfig.UseStaticIcons = true;
config.ProcessingConfig.IconConfig.ScanRotatedIcons = false;

using RatEyeEngine engine = new(config, RatStash.Database.FromItems([expected, broken]));
using RatEyeEngine engine = new(config, RatStash.Database.FromItems([expected, broken, blank]));
engine.Config.IconManager.EnsureStaticIconsLoaded(new Vector2(1, 1));
KeyValuePair<string, Mat> loaded = Assert.Single(engine.Config.IconManager.StaticIcons[new Vector2(1, 1)]);
Assert.EndsWith("fixture.png", loaded.Key, StringComparison.Ordinal);
Expand Down
5 changes: 5 additions & 0 deletions tests/RatScanner.Tests/PresentationServicesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ public void Static_icons_are_loaded_one_slot_size_at_a_time()
private static void WriteIcon(string path, int width, int height)
{
using System.Drawing.Bitmap bitmap = new(width, height);
using (System.Drawing.Graphics graphics = System.Drawing.Graphics.FromImage(bitmap))
{
using System.Drawing.Brush brush = new System.Drawing.SolidBrush(System.Drawing.Color.White);
graphics.FillEllipse(brush, width / 4, height / 4, width / 2, height / 2);
}
bitmap.Save(path, System.Drawing.Imaging.ImageFormat.Png);
}
}
Expand Down
Loading