rwork on kulemak bot and cleanup
This commit is contained in:
parent
c75b2b27f0
commit
053a016c8b
15 changed files with 727 additions and 160 deletions
|
|
@ -18,6 +18,8 @@ public interface IScreenReader : IDisposable
|
|||
Task<DiffOcrResponse> DiffOcr(string? savePath = null, Region? region = null);
|
||||
Task<TemplateMatchResult?> TemplateMatch(string templatePath, Region? region = null);
|
||||
Task<OcrResponse> NameplateDiffOcr(System.Drawing.Bitmap reference, System.Drawing.Bitmap current);
|
||||
void SetLootBaseline(System.Drawing.Bitmap frame);
|
||||
List<LootLabel> DetectLootLabels(System.Drawing.Bitmap reference, System.Drawing.Bitmap current);
|
||||
System.Drawing.Bitmap CaptureRawBitmap();
|
||||
Task SaveScreenshot(string path);
|
||||
Task SaveRegion(Region region, string path);
|
||||
|
|
|
|||
110
src/Poe2Trade.Screen/LootLabel.cs
Normal file
110
src/Poe2Trade.Screen/LootLabel.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
namespace Poe2Trade.Screen;
|
||||
|
||||
/// <summary>
|
||||
/// A detected loot label on screen with its position and classified tier.
|
||||
/// </summary>
|
||||
public record LootLabel(int CenterX, int CenterY, int Width, int Height, string Tier, byte AvgR, byte AvgG, byte AvgB);
|
||||
|
||||
/// <summary>
|
||||
/// Classifies loot label background colors to NeverSink filter tiers
|
||||
/// by matching against known filter color palette.
|
||||
/// </summary>
|
||||
public static class LootColorClassifier
|
||||
{
|
||||
private record ColorEntry(byte R, byte G, byte B, string Tier);
|
||||
|
||||
// Background colors from NeverSink's Uber Strict filter
|
||||
private static readonly ColorEntry[] KnownBgColors =
|
||||
[
|
||||
// S-tier (apex): white bg
|
||||
new(255, 255, 255, "S"),
|
||||
// A-tier: red bg, white text
|
||||
new(245, 105, 90, "A"),
|
||||
// C-tier: orange bg
|
||||
new(245, 139, 87, "C"),
|
||||
// D-tier: yellow bg
|
||||
new(240, 180, 100, "D"),
|
||||
// E-tier: text-only, yellowish
|
||||
new(240, 207, 132, "E"),
|
||||
|
||||
// Unique high: brown bg
|
||||
new(188, 96, 37, "unique"),
|
||||
// Unique T3: dark red bg
|
||||
new(53, 13, 13, "unique-low"),
|
||||
|
||||
// Exotic bases: dark green bg
|
||||
new(0, 75, 30, "exotic"),
|
||||
// Identified mods: dark purple bg
|
||||
new(47, 0, 74, "exotic-mod"),
|
||||
|
||||
// Rare jewellery: olive bg
|
||||
new(75, 75, 0, "rare-jewellery"),
|
||||
|
||||
// Fragments: bright purple bg
|
||||
new(220, 0, 255, "fragment"),
|
||||
// Fragments lower: light purple bg
|
||||
new(180, 75, 225, "fragment"),
|
||||
// Fragment splinter: dark purple bg
|
||||
new(50, 0, 75, "fragment"),
|
||||
|
||||
// Maps special: lavender bg
|
||||
new(235, 220, 245, "map"),
|
||||
// Maps regular high: light grey bg
|
||||
new(235, 235, 235, "map"),
|
||||
// Maps regular: grey bg
|
||||
new(200, 200, 200, "map"),
|
||||
|
||||
// Crafting magic: dark blue-purple bg
|
||||
new(30, 0, 70, "crafting"),
|
||||
|
||||
// Gems: cyan text (20,240,240) - no bg
|
||||
new(20, 240, 240, "gem"),
|
||||
// Gems: dark blue bg
|
||||
new(6, 0, 60, "gem"),
|
||||
|
||||
// Flasks/charms: dark green bg
|
||||
new(10, 60, 40, "flask"),
|
||||
|
||||
// Currency artifact: dark brown bg
|
||||
new(76, 51, 12, "artifact"),
|
||||
|
||||
// Socketables (runes): orange-tan bg
|
||||
new(220, 175, 132, "socketable"),
|
||||
|
||||
// Gold drops: gold/yellow text
|
||||
new(180, 160, 80, "gold"),
|
||||
new(200, 180, 100, "gold"),
|
||||
|
||||
// Pink/magenta catch-all (e.g. boss-specific drops like invitations)
|
||||
new(255, 0, 255, "special"),
|
||||
new(220, 50, 220, "special"),
|
||||
];
|
||||
|
||||
private const double MaxDistance = 50.0;
|
||||
|
||||
/// <summary>
|
||||
/// Classify an average RGB color to the closest NeverSink filter tier.
|
||||
/// Returns "unknown" if no known color is within MaxDistance.
|
||||
/// </summary>
|
||||
public static string Classify(byte avgR, byte avgG, byte avgB)
|
||||
{
|
||||
double bestDist = double.MaxValue;
|
||||
string bestTier = "unknown";
|
||||
|
||||
foreach (var entry in KnownBgColors)
|
||||
{
|
||||
double dr = avgR - entry.R;
|
||||
double dg = avgG - entry.G;
|
||||
double db = avgB - entry.B;
|
||||
double dist = Math.Sqrt(dr * dr + dg * dg + db * db);
|
||||
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestTier = entry.Tier;
|
||||
}
|
||||
}
|
||||
|
||||
return bestDist <= MaxDistance ? bestTier : "unknown";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Runtime.InteropServices;
|
||||
using Poe2Trade.Core;
|
||||
using OpenCvSharp;
|
||||
using OpenCvSharp.Extensions;
|
||||
using Poe2Trade.Core;
|
||||
using Serilog;
|
||||
using Region = Poe2Trade.Core.Region;
|
||||
using Size = OpenCvSharp.Size;
|
||||
|
||||
namespace Poe2Trade.Screen;
|
||||
|
||||
|
|
@ -320,6 +322,78 @@ public class ScreenReader : IScreenReader
|
|||
return boxes;
|
||||
}
|
||||
|
||||
// -- Loot label detection (magenta background) --
|
||||
//
|
||||
// All loot labels: white border, magenta (255,0,255) background, black text.
|
||||
// Magenta never appears in the game world → detect directly, no diff needed.
|
||||
|
||||
public void SetLootBaseline(Bitmap frame) { }
|
||||
|
||||
public List<LootLabel> DetectLootLabels(Bitmap reference, Bitmap current)
|
||||
{
|
||||
using var mat = BitmapConverter.ToMat(current);
|
||||
if (mat.Channels() == 4)
|
||||
Cv2.CvtColor(mat, mat, ColorConversionCodes.BGRA2BGR);
|
||||
|
||||
// Mask magenta background pixels (BGR: B≈255, G≈0, R≈255)
|
||||
using var mask = new Mat();
|
||||
Cv2.InRange(mat, new Scalar(200, 0, 200), new Scalar(255, 60, 255), mask);
|
||||
|
||||
// Morph close fills text gaps within a label
|
||||
// Height=2 bridges line gaps within multi-line labels but not between separate labels
|
||||
using var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(12, 2));
|
||||
using var closed = new Mat();
|
||||
Cv2.MorphologyEx(mask, closed, MorphTypes.Close, kernel);
|
||||
|
||||
// Save debug images
|
||||
try
|
||||
{
|
||||
Cv2.ImWrite("debug_loot_mask.png", mask);
|
||||
Cv2.ImWrite("debug_loot_closed.png", closed);
|
||||
current.Save("debug_loot_capture.png", System.Drawing.Imaging.ImageFormat.Png);
|
||||
Log.Information("Saved debug images: debug_loot_mask.png, debug_loot_closed.png, debug_loot_capture.png");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to save debug images");
|
||||
}
|
||||
|
||||
Cv2.FindContours(closed, out var contours, out _,
|
||||
RetrievalModes.External, ContourApproximationModes.ApproxSimple);
|
||||
|
||||
Log.Information("DetectLootLabels: {N} magenta contours", contours.Length);
|
||||
|
||||
const int minW = 40, maxW = 600;
|
||||
const int minH = 8, maxH = 100;
|
||||
const double minAspect = 1.5;
|
||||
int yMax = mat.Height - 210;
|
||||
|
||||
var labels = new List<LootLabel>();
|
||||
foreach (var contour in contours)
|
||||
{
|
||||
var box = Cv2.BoundingRect(contour);
|
||||
double aspect = box.Height > 0 ? (double)box.Width / box.Height : 0;
|
||||
|
||||
if (box.Width < minW || box.Width > maxW ||
|
||||
box.Height < minH || box.Height > maxH ||
|
||||
aspect < minAspect ||
|
||||
box.Y < 65 || box.Y + box.Height > yMax)
|
||||
{
|
||||
Log.Information("Rejected contour: ({X},{Y}) {W}x{H} aspect={Aspect:F1} yMax={YMax}",
|
||||
box.X, box.Y, box.Width, box.Height, aspect, yMax);
|
||||
continue;
|
||||
}
|
||||
|
||||
int cx = box.X + box.Width / 2;
|
||||
int cy = box.Y + box.Height / 2;
|
||||
|
||||
Log.Information("Label at ({X},{Y}) {W}x{H}", box.X, box.Y, box.Width, box.Height);
|
||||
labels.Add(new LootLabel(cx, cy, box.Width, box.Height, "loot", 255, 0, 255));
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
public void Dispose() => _pythonBridge.Dispose();
|
||||
|
||||
// -- OCR text matching --
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue