using Godot; using System; using System.Collections.Generic; using System.IO; using OpenCvSharp; using ETPreferences; using System.Globalization; /// /// Standalone pupil detection test that can run in a blank Godot scene. /// This is useful for testing and debugging pupil detection independently. /// public partial class CheckPupilAlignment : Node { [Export] public Node3D MoveUpQuad { get; set; } [Export] public Node3D MoveDownQuad { get; set; } [Export] public Node3D GoodQuad { get; set; } [Export] public MeshInstance3D PreviewQuad { get; set; } [Export] public MeshInstance3D TextMesh { get; set; } [Export] public float AlignmentThreshold { get; set; } = 0.02f; // Normalized coordinates (0-1) [Export] public bool AutoCompare { get; set; } = true; [Export] public float CompareInterval { get; set; } = 1.0f; // Seconds between checks private CameraManager _cameraManager; private float _compareTimer = 0f; private TextMesh TextMeshInstance; private StandardMaterial3D PreviewMat; public override void _Ready() { OpenXRManager.EnableOpenXR(this); // Create and initialize camera manager _cameraManager = new CameraManager(); AddChild(_cameraManager); // Start camera initialization InitializeCamera(); EyeTrackingPreferences.ReadSettingsFile(); TextMeshInstance = (TextMesh)TextMesh.Mesh; } private async void InitializeCamera() { await _cameraManager.InitializeCamera(); } public override void _Process(double delta) { if (AutoCompare && _cameraManager != null && _cameraManager.IsInitialized) { _compareTimer += (float)delta; if (_compareTimer >= CompareInterval) { _compareTimer = 0f; CompareToSaved(); } } } public Image MatToGodotImage(Mat mat) { // Convert colors Mat rgb = new Mat(); Cv2.CvtColor(mat, rgb, ColorConversionCodes.BGR2RGB); // Copy to Godot image byte[] data = new byte[rgb.Total() * rgb.ElemSize()]; System.Runtime.InteropServices.Marshal.Copy(rgb.Data, data, 0, data.Length); var image = new Image(); image.SetData(rgb.Cols, rgb.Rows, false, Image.Format.Rgb8, data); rgb.Dispose(); return image; } /// /// Detects pupil centers and compares them to saved values. /// public void CompareToSaved() { if (_cameraManager == null || !_cameraManager.IsInitialized) { return; } // Hide all quads initially if (MoveUpQuad != null) MoveUpQuad.Visible = false; if (MoveDownQuad != null) MoveDownQuad.Visible = false; if (GoodQuad != null) GoodQuad.Visible = false; try { // Get saved values float savedLeftX = ET_Preferences.Instance.left_pupil_center_x; float savedLeftY = ET_Preferences.Instance.left_pupil_center_y; float savedRightX = ET_Preferences.Instance.right_pupil_center_x; float savedRightY = ET_Preferences.Instance.right_pupil_center_y; // Get a fresh color frame bool wasGrayscale = _cameraManager.UseGrayscale; _cameraManager.UseGrayscale = false; // Flush buffer by grabbing a few frames to ensure we get the latest Mat colorFrame = null; for (int i = 0; i < 3; i++) { colorFrame?.Dispose(); colorFrame = _cameraManager.GetCameraFrame(); } _cameraManager.UseGrayscale = wasGrayscale; if (colorFrame == null || colorFrame.Empty()) { return; } // Detect pupils List pupils = PupilDetector.FindPupilCenters(colorFrame); if (pupils.Count == 2) { // Calculate current normalized values float currentLeftX = pupils[0].X / (float)colorFrame.Width; float currentLeftY = pupils[0].Y / (float)colorFrame.Height; float currentRightX = pupils[1].X / (float)colorFrame.Width; float currentRightY = pupils[1].Y / (float)colorFrame.Height; // Calculate differences float diffLeftX = currentLeftX - savedLeftX; float diffLeftY = currentLeftY - savedLeftY; float diffRightX = currentRightX - savedRightX; float diffRightY = currentRightY - savedRightY; // Calculate pixel differences for reference float pixelDiffLeftX = diffLeftX * colorFrame.Width; float pixelDiffLeftY = diffLeftY * colorFrame.Height; float pixelDiffRightX = diffRightX * colorFrame.Width; float pixelDiffRightY = diffRightY * colorFrame.Height; // Calculate total distance (Euclidean) float leftDistance = Mathf.Sqrt(diffLeftX * diffLeftX + diffLeftY * diffLeftY); float rightDistance = Mathf.Sqrt(diffRightX * diffRightX + diffRightY * diffRightY); float leftDistancePixels = Mathf.Sqrt(pixelDiffLeftX * pixelDiffLeftX + pixelDiffLeftY * pixelDiffLeftY); float rightDistancePixels = Mathf.Sqrt(pixelDiffRightX * pixelDiffRightX + pixelDiffRightY * pixelDiffRightY); // Show appropriate quad based on average Y position difference // Note: In screen coordinates, lower Y = higher on screen, higher Y = lower on screen float avgCurrentY = (currentLeftY + currentRightY) / 2.0f; float avgSavedY = (savedLeftY + savedRightY) / 2.0f; float avgDiffY = avgCurrentY - avgSavedY; // Test visuals ImageTexture texture = ImageTexture.CreateFromImage(MatToGodotImage(colorFrame)); var mat = PreviewQuad.MaterialOverride as StandardMaterial3D ?? new StandardMaterial3D(); mat.AlbedoTexture = texture; PreviewQuad.MaterialOverride = mat; TextMeshInstance.Text = "avgDiffY: " + avgDiffY.ToString(CultureInfo.InvariantCulture); if (avgDiffY < -AlignmentThreshold) // Current pupils are higher on screen (beyond threshold) { if (MoveUpQuad != null) { MoveUpQuad.Visible = true; } } else if (avgDiffY > AlignmentThreshold) // Current pupils are lower on screen (beyond threshold) { if (MoveDownQuad != null) { MoveDownQuad.Visible = true; } } else // Within acceptable threshold { if (GoodQuad != null) { GoodQuad.Visible = true; } } } colorFrame.Dispose(); } catch (Exception ex) { GD.PrintErr($"Error during pupil comparison: {ex.Message}"); } } public void SaveToPreferences() { EyeTrackingPreferences.WriteSettingsFile(); } public override void _ExitTree() { if (_cameraManager != null) { _cameraManager.ReleaseCamera(); _cameraManager.QueueFree(); } base._ExitTree(); } }