Skip to content

Commit eb20ff2

Browse files
committed
🐛 fix tests
semver: patch
1 parent 14ea520 commit eb20ff2

36 files changed

Lines changed: 3033 additions & 7 deletions

.claude/settings.local.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(dotnet build:*)",
5+
"WebSearch",
6+
"WebFetch(domain:github.com)",
7+
"Read(//c/Users/Lucas/EDAPGui/**)",
8+
"Bash(del \"C:\\Users\\Lucas\\eliteapi\\EliteAI\\UI\\Navball.cs\")",
9+
"Read(//c/Users/Lucas/**)",
10+
"Bash(cat:*)",
11+
"Bash(dotnet restore)",
12+
"Bash(dotnet test)",
13+
"Bash(findstr:*)",
14+
"Bash(dotnet test:*)"
15+
],
16+
"deny": [],
17+
"ask": []
18+
}
19+
}

EliteAI/AutopilotProgram.cs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
using OpenCvSharp;
2+
using EliteAI.Capture;
3+
using EliteAI.Input;
4+
5+
namespace EliteAI;
6+
7+
/// <summary>
8+
/// Autopilot program with navball/target detection and flight control
9+
/// </summary>
10+
public class AutopilotProgram
11+
{
12+
public static void Run()
13+
{
14+
var regions = new ScreenRegions(
15+
EliteAI.Capture.ScreenCapture.ScreenWidth,
16+
EliteAI.Capture.ScreenCapture.ScreenHeight);
17+
18+
// Only load templates for non-navball regions (navball uses circle detection)
19+
regions.LoadTemplates();
20+
21+
// Create target detector (handles both target marker and navball)
22+
var target = regions.CreateTarget();
23+
24+
// Create autopilot (0.05 = 5% deadzone)
25+
var autopilot = new SimpleAutopilot(deadzone: 0.05f);
26+
bool autopilotEnabled = false;
27+
28+
// Create global hotkey for 'P' (VK_P = 0x50)
29+
var toggleHotkey = new GlobalHotkey(0x50);
30+
31+
Console.WriteLine("Press 'P' to toggle autopilot (works from any window!), 'q' to quit");
32+
33+
while (true)
34+
{
35+
using var frame = EliteAI.Capture.ScreenCapture.GetFrame();
36+
37+
// Get the best available target (tries target marker first, falls back to navball)
38+
var aimResult = target.AimAtTarget(frame);
39+
40+
// Visualization: Draw navball if detected
41+
var navballCircle = target.GetNavballCircle(frame);
42+
if (navballCircle != null)
43+
{
44+
// Draw the detected navball circle
45+
Cv2.Circle(frame, (Point)navballCircle.Center, (int)navballCircle.Radius, Scalar.LimeGreen, 2);
46+
Cv2.Circle(frame, (Point)navballCircle.Center, 2, Scalar.Red, 3); // Center point
47+
48+
// If using navball for aiming, show the target dot
49+
if (aimResult.HasValue && aimResult.Value.source == "Navball")
50+
{
51+
var (x, y, _) = aimResult.Value;
52+
var targetScreenX = navballCircle.Center.X + (x * navballCircle.Radius);
53+
var targetScreenY = navballCircle.Center.Y + (y * navballCircle.Radius);
54+
55+
Cv2.Circle(frame, new Point((int)targetScreenX, (int)targetScreenY), 5, Scalar.Cyan, -1);
56+
57+
// Show blue filter debug window
58+
var center = navballCircle.Center;
59+
var radius = navballCircle.Radius;
60+
var regionSize = (int)(radius * 2.2);
61+
var regionLeft = (int)(center.X - regionSize / 2);
62+
var regionTop = (int)(center.Y - regionSize / 2);
63+
regionLeft = Math.Max(0, Math.Min(regionLeft, EliteAI.Capture.ScreenCapture.ScreenWidth - regionSize));
64+
regionTop = Math.Max(0, Math.Min(regionTop, EliteAI.Capture.ScreenCapture.ScreenHeight - regionSize));
65+
66+
var debugRect = new Rect(regionLeft, regionTop, regionSize, regionSize);
67+
using var navballRegion = new Mat(frame, debugRect);
68+
using var blueFiltered = Filters.FilterByColor(navballRegion, Filters.BlueColorRange);
69+
Cv2.ImShow("Blue Filtered (Target Dot)", blueFiltered);
70+
}
71+
}
72+
73+
// Visualization: Draw target marker if detected (regardless of being used)
74+
var targetMarkerPos = target.GetTargetMarker(frame);
75+
if (targetMarkerPos.HasValue)
76+
{
77+
var (x, y) = targetMarkerPos.Value;
78+
var screenCenterX = EliteAI.Capture.ScreenCapture.ScreenWidth / 2f;
79+
var screenCenterY = EliteAI.Capture.ScreenCapture.ScreenHeight / 2f;
80+
81+
// Calculate target position from normalized coordinates
82+
var targetScreenX = screenCenterX + (x * screenCenterX * 0.5f);
83+
var targetScreenY = screenCenterY + (y * screenCenterY * 0.5f);
84+
85+
// Draw the detected target marker (magenta circle)
86+
Cv2.Circle(frame, new Point((int)targetScreenX, (int)targetScreenY), 10, Scalar.Magenta, 3);
87+
88+
// Draw crosshair at screen center
89+
Cv2.Line(frame, new Point((int)screenCenterX - 20, (int)screenCenterY),
90+
new Point((int)screenCenterX + 20, (int)screenCenterY), Scalar.Yellow, 2);
91+
Cv2.Line(frame, new Point((int)screenCenterX, (int)screenCenterY - 20),
92+
new Point((int)screenCenterX, (int)screenCenterY + 20), Scalar.Yellow, 2);
93+
}
94+
95+
// Debug: Show Target search region and filtered view
96+
var targetRegion = regions.Regions[RegionName.Target];
97+
var targetRect = targetRegion.GetRect(EliteAI.Capture.ScreenCapture.ScreenWidth, EliteAI.Capture.ScreenCapture.ScreenHeight);
98+
Cv2.Rectangle(frame, targetRect, Scalar.Orange, 1);
99+
100+
// Show the orange-filtered region being used for detection
101+
using var targetSearchRegion = new Mat(frame, targetRect);
102+
using var orangeFiltered = Filters.FilterByColor(targetSearchRegion, Filters.Orange2ColorRange);
103+
Cv2.ImShow("Target Orange Filtered", orangeFiltered);
104+
105+
// Update autopilot with the best available target
106+
if (aimResult.HasValue)
107+
{
108+
var (x, y, source) = aimResult.Value;
109+
110+
if (autopilotEnabled)
111+
{
112+
autopilot.Update(x, y);
113+
Console.WriteLine($"{source}: Target=({x:F2}, {y:F2}) | AUTOPILOT ACTIVE");
114+
}
115+
else
116+
{
117+
Console.WriteLine($"{source}: Target=({x:F2}, {y:F2})");
118+
}
119+
}
120+
else
121+
{
122+
Console.WriteLine("No target detected");
123+
if (autopilotEnabled)
124+
autopilot.Stop();
125+
}
126+
127+
// Draw region bounds
128+
var searchRect = target.SearchRegion;
129+
Cv2.Rectangle(frame, searchRect, new Scalar(0, 255, 255), 1);
130+
131+
// Draw autopilot status
132+
var statusLabel = autopilotEnabled ? "AUTOPILOT: ON (P to disable)" : "AUTOPILOT: OFF (P to enable)";
133+
Cv2.PutText(frame, statusLabel, new Point(10, 30),
134+
HersheyFonts.HersheySimplex, 0.7, autopilotEnabled ? Scalar.LimeGreen : Scalar.Red, 2);
135+
136+
// Show full screen at 50% size
137+
using var resizedFrame = new Mat();
138+
Cv2.Resize(frame, resizedFrame, new Size(), 0.5, 0.5);
139+
Cv2.ImShow("NavBall Target Detection", resizedFrame);
140+
141+
// Check for global hotkey press
142+
if (toggleHotkey.WasPressed())
143+
{
144+
autopilotEnabled = !autopilotEnabled;
145+
if (!autopilotEnabled)
146+
autopilot.Stop();
147+
Console.WriteLine($"Autopilot {(autopilotEnabled ? "ENABLED" : "DISABLED")}");
148+
}
149+
150+
// Wait 1ms for key press (for 'q' to quit)
151+
var key = Cv2.WaitKey(1);
152+
153+
// Break on 'q' or ESC
154+
if (key is 'q' or 27) break;
155+
}
156+
157+
// Clean up
158+
autopilot.Stop();
159+
regions.DisposeTemplates();
160+
Cv2.DestroyAllWindows();
161+
Console.WriteLine("Autopilot stopped.");
162+
}
163+
}

EliteAI/Capture/Navball.cs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
using OpenCvSharp;
2+
3+
namespace EliteAI.Capture;
4+
5+
/// <summary>
6+
/// Represents a detected circle from Hough Circle Transform
7+
/// </summary>
8+
public class DetectedCircle
9+
{
10+
public Point2f Center { get; init; }
11+
public float Radius { get; init; }
12+
}
13+
14+
/// <summary>
15+
/// Navball/compass detector using Hough Circle Transform.
16+
/// Much more robust than template matching since the navball is a circular shape.
17+
/// </summary>
18+
public class NavballDetector
19+
{
20+
private readonly int _screenWidth;
21+
private readonly int _screenHeight;
22+
private readonly Rect _searchRegion;
23+
24+
// Temporal smoothing
25+
private DetectedCircle? _lastDetectedCircle = null;
26+
private int _notDetectedFrames = 0;
27+
private const int MaxMissedFrames = 5; // Allow up to 5 frames of missed detection before losing track
28+
29+
public NavballDetector(int screenWidth, int screenHeight, double[] regionBounds)
30+
{
31+
_screenWidth = screenWidth;
32+
_screenHeight = screenHeight;
33+
34+
// Convert normalized bounds to pixel coordinates
35+
var left = (int)(regionBounds[0] * screenWidth);
36+
var top = (int)(regionBounds[1] * screenHeight);
37+
var right = (int)(regionBounds[2] * screenWidth);
38+
var bottom = (int)(regionBounds[3] * screenHeight);
39+
40+
_searchRegion = new Rect(left, top, right - left, bottom - top);
41+
}
42+
43+
/// <summary>
44+
/// Finds the navball using Hough Circle Transform with temporal smoothing.
45+
/// </summary>
46+
/// <param name="fullScreen">Full screen capture</param>
47+
/// <returns>Circle if found, null otherwise</returns>
48+
public DetectedCircle? FindNavball(Mat fullScreen)
49+
{
50+
// Extract the navball region
51+
using var regionMat = new Mat(fullScreen, _searchRegion);
52+
53+
// Filter for orange color to isolate the navball's orange elements
54+
using var orangeFiltered = Filters.FilterByColor(regionMat, Filters.OrangeColorRange);
55+
56+
// Debug: Show the filtered result
57+
Cv2.ImShow("Navball Orange Filtered", orangeFiltered);
58+
59+
// Apply Gaussian blur to reduce noise
60+
using var blurred = new Mat();
61+
Cv2.GaussianBlur(orangeFiltered, blurred, new Size(9, 9), 2);
62+
63+
// Detect circles using Hough Circle Transform
64+
// Parameters tuned for the navball which has clear circular edges
65+
// Expected radius: ~34 pixels
66+
var circles = Cv2.HoughCircles(
67+
blurred,
68+
HoughModes.Gradient,
69+
dp: 1, // Inverse ratio of accumulator resolution
70+
minDist: 50, // Minimum distance between circle centers
71+
param1: 50, // Canny edge detection threshold (lower = more edges)
72+
param2: 20, // Accumulator threshold for circle detection (lower = more circles)
73+
minRadius: 25, // Minimum circle radius (lowered from 25)
74+
maxRadius: 45 // Maximum circle radius (increased from 45)
75+
);
76+
77+
Console.WriteLine($"[Navball] Hough detected {circles.Length} circles");
78+
if (circles.Length > 0)
79+
{
80+
for (int i = 0; i < Math.Min(3, circles.Length); i++)
81+
{
82+
Console.WriteLine($" Circle {i}: center=({circles[i].Center.X:F1}, {circles[i].Center.Y:F1}), radius={circles[i].Radius:F1}");
83+
}
84+
}
85+
86+
DetectedCircle? detectedCircle = null;
87+
88+
if (circles.Length > 0)
89+
{
90+
// If we have a previous detection, find the circle closest to it
91+
if (_lastDetectedCircle != null && circles.Length > 1)
92+
{
93+
var bestCircle = circles[0];
94+
var minDistance = float.MaxValue;
95+
96+
foreach (var c in circles)
97+
{
98+
var screenPos = new Point2f(_searchRegion.X + c.Center.X, _searchRegion.Y + c.Center.Y);
99+
var distance = Math.Sqrt(
100+
Math.Pow(screenPos.X - _lastDetectedCircle.Center.X, 2) +
101+
Math.Pow(screenPos.Y - _lastDetectedCircle.Center.Y, 2));
102+
103+
if (distance < minDistance)
104+
{
105+
minDistance = (float)distance;
106+
bestCircle = c;
107+
}
108+
}
109+
110+
detectedCircle = new DetectedCircle
111+
{
112+
Center = new Point2f(_searchRegion.X + bestCircle.Center.X, _searchRegion.Y + bestCircle.Center.Y),
113+
Radius = bestCircle.Radius
114+
};
115+
}
116+
else
117+
{
118+
// Use the first (strongest) circle
119+
var circle = circles[0];
120+
detectedCircle = new DetectedCircle
121+
{
122+
Center = new Point2f(_searchRegion.X + circle.Center.X, _searchRegion.Y + circle.Center.Y),
123+
Radius = circle.Radius
124+
};
125+
}
126+
127+
// Smooth the position with previous detection
128+
if (_lastDetectedCircle != null)
129+
{
130+
const float smoothingFactor = 0.7f; // 0.7 = use 70% new, 30% old
131+
detectedCircle = new DetectedCircle
132+
{
133+
Center = new Point2f(
134+
_lastDetectedCircle.Center.X * (1 - smoothingFactor) + detectedCircle.Center.X * smoothingFactor,
135+
_lastDetectedCircle.Center.Y * (1 - smoothingFactor) + detectedCircle.Center.Y * smoothingFactor
136+
),
137+
Radius = _lastDetectedCircle.Radius * (1 - smoothingFactor) + detectedCircle.Radius * smoothingFactor
138+
};
139+
}
140+
141+
_lastDetectedCircle = detectedCircle;
142+
_notDetectedFrames = 0;
143+
}
144+
else
145+
{
146+
// Not detected this frame
147+
_notDetectedFrames++;
148+
149+
// If we recently had a detection, keep returning it for a few frames
150+
if (_notDetectedFrames <= MaxMissedFrames && _lastDetectedCircle != null)
151+
{
152+
detectedCircle = _lastDetectedCircle;
153+
}
154+
else
155+
{
156+
_lastDetectedCircle = null;
157+
}
158+
}
159+
160+
return detectedCircle;
161+
}
162+
163+
/// <summary>
164+
/// Gets the search region rectangle
165+
/// </summary>
166+
public Rect SearchRegion => _searchRegion;
167+
}

EliteAI/Capture/ScreenCapture.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using OpenCvSharp;
2+
using ScreenCapture.NET;
3+
4+
namespace EliteAI.Capture;
5+
6+
public static class ScreenCapture
7+
{
8+
private static readonly DX11ScreenCaptureService CaptureService = new();
9+
private static readonly GraphicsCard Card = CaptureService.GetGraphicsCards().First();
10+
private static readonly Display Display = CaptureService.GetDisplays(Card).First();
11+
private static readonly DX11ScreenCapture Capture = CaptureService.GetScreenCapture(Display);
12+
private static readonly ICaptureZone Zone = Capture.RegisterCaptureZone(0, 0, Display.Width, Display.Height);
13+
14+
public static int ScreenWidth => Display.Width;
15+
public static int ScreenHeight => Display.Height;
16+
17+
public static Mat GetFrame()
18+
{
19+
Capture.CaptureScreen();
20+
21+
using (Zone.Lock())
22+
{
23+
var image = Zone.Image;
24+
var rawBuffer = Zone.RawBuffer;
25+
26+
// Create Mat from captured buffer (BGRA format from DX11)
27+
unsafe
28+
{
29+
fixed (byte* ptr = rawBuffer)
30+
{
31+
using var bgraMat = Mat.FromPixelData(Display.Height, Display.Width, MatType.CV_8UC4, (IntPtr)ptr);
32+
33+
// Convert BGRA to BGR for standard OpenCV processing
34+
var bgrMat = new Mat();
35+
Cv2.CvtColor(bgraMat, bgrMat, ColorConversionCodes.BGRA2BGR);
36+
37+
return bgrMat;
38+
}
39+
}
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)