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();
}
}