rwork on kulemak bot and cleanup

This commit is contained in:
Boki 2026-02-21 09:18:10 -05:00
parent c75b2b27f0
commit 053a016c8b
15 changed files with 727 additions and 160 deletions

View file

@ -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);

View 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";
}
}

View file

@ -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 --