/* * SPDX-License-Identifier: AGPL-3.0-or-later * Copyright (C) 2025 Sergej Görzen * This file is part of OmiLAXR. */ using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using UnityEngine; namespace OmiLAXR.TrackingBehaviours.Learner.HeadTracking { /// /// Utility class for head gesture recognition and analysis. /// Provides algorithms for detecting nodding and shaking gestures from position data. /// public static class HeadUtils { /// /// Counts gestures in float coordinate list by detecting peaks above minimum amplitude. /// /// List of float coordinates to analyze /// Minimum amplitude required for gesture detection /// Number of detected gestures public static int GetNumOfGestures(List listOfCoordinates, double minAmplitude) { var doubleList = listOfCoordinates.ConvertAll(x => (double)x); return GetNumOfGestures(doubleList, minAmplitude); } /// /// Counts gestures in HMD position list along specified axis. /// /// List of HMD positions with timestamps /// Minimum amplitude required for gesture detection /// Axis to analyze ("x", "y", or "z") /// Number of detected gestures public static int GetNumOfGestures(List listOfCoordinates, double minAmplitude, string axis) { var doubleList = ConvertHmdPositionListIntoDoubleList(listOfCoordinates, axis); return GetNumOfGestures(doubleList, minAmplitude); } /// /// Core gesture counting algorithm using peak detection on smoothed data. /// /// List of coordinates as doubles /// Minimum amplitude for peak detection /// Maximum number of peaks or valleys detected private static int GetNumOfGestures(List listOfCoordinates, double minAmplitude) { //smooth line for correct calculation of number of head shakes var smoothedLine = SmoothLine(listOfCoordinates, minAmplitude); //get max number of maxima and minima values var maxPeaks = FindMaxPeaks(smoothedLine); var minPeaks = FindMinPeaks(smoothedLine); //return the max number of maxima/minima values return Mathf.Max(minPeaks.Count, maxPeaks.Count); } /// /// Detects if gesture has finished by checking movement continuity. /// /// List of HMD positions to check /// Starting position for continuity check /// Minimum distance threshold for gesture completion /// Axis to analyze ("x", "y", or "z") /// True if gesture has finished public static bool GestureFinished(List listOfCoordinates, int positionContinuousElement, double minDistance, string axis) { var floatList = ConvertHmdPositionListIntoFloatList(listOfCoordinates, axis); return GestureFinished(floatList, positionContinuousElement, minDistance); } /// /// Checks if recent movement amplitude is below threshold (gesture finished). /// /// List of coordinate values /// Starting position for range check /// Minimum distance threshold /// True if movement amplitude is below threshold public static bool GestureFinished(List listOfCoordinates, int positionContinuousElement, double minDistance) { //get the maximum and minimum of sublist for continuous gesture check var maxSubDistance = listOfCoordinates.GetRange(positionContinuousElement, listOfCoordinates.Count - positionContinuousElement).Max(); var minSubDistance = listOfCoordinates.GetRange(positionContinuousElement, listOfCoordinates.Count - positionContinuousElement).Min(); return Math.Abs(maxSubDistance - minSubDistance) < minDistance; } /// /// Checks if movement amplitude is sufficient for gesture recognition. /// /// List of HMD positions to analyze /// Minimum required amplitude /// Axis to check ("x", "y", or "z") /// True if amplitude is sufficient public static bool EnoughAmplitude(List listOfCoordinates, double minAmplitude, string axis) { var floatList = ConvertHmdPositionListIntoFloatList(listOfCoordinates, axis); return EnoughAmplitude(floatList, minAmplitude); } /// /// Verifies if amplitude between min/max values exceeds threshold. /// /// List of values to check /// Minimum amplitude threshold /// True if amplitude exceeds threshold public static bool EnoughAmplitude(List list, double minAmplitude) { if (list.Count <= 0) return false; var amplitude = Mathf.Abs(list.Max() - list.Min()); return amplitude >= minAmplitude; } /// /// Smooths position data by filtering out small movements below threshold. /// /// Input distance values /// Minimum distance for significant movement /// Smoothed list with only significant changes private static List SmoothLine(IReadOnlyList distances, double minDistanceGesture) { var distancesSmoothed = new List { distances[0] }; var startValue = distances[0]; foreach (var t in distances) { var res = Math.Abs(startValue - t); if (!(res >= minDistanceGesture)) continue; distancesSmoothed.Add(t); startValue = t; } return distancesSmoothed; } /// /// Finds start position for continuous gesture detection within time window. /// /// List of timestamped positions /// Maximum time span for continuous gesture /// Index of start position, or -1 if not found public static int GetPositionContinuousGesture(List list, TimeSpan continuousTimeBetweenGestures) { for (var i = list.Count - 1; i >= 0; i--) { if ((list[list.Count - 1].Timestamp - list[i].Timestamp) > continuousTimeBetweenGestures) return i; } return -1; } /// /// Clears position tracking lists. /// /// HMD position list to clear public static void ClearAllItems(ref List hmdPositions) { var helpPointsOfFocus = new List(); ClearAllItems(ref hmdPositions, ref helpPointsOfFocus); } /// /// Overload for clearing multiple tracking lists. /// /// HMD position list to clear /// Points of focus list to clear private static void ClearAllItems(ref List hmdPositions, ref List pointsOfFocus) { var helpHmdPositionsManipulated = new List(); var helpDistanceHmdPositionsManipulated = new List(); ClearAllItems(ref hmdPositions, ref helpHmdPositionsManipulated, ref helpDistanceHmdPositionsManipulated, ref pointsOfFocus); } /// /// Comprehensive list clearing for tracking data. /// /// HMD position list to clear /// Manipulated HMD position list to clear /// Distance list to clear public static void ClearAllItems(ref List hmdPositions, ref List hmdPositionsManipulated, ref List distanceHmdPositionsManipulated) { var helpPointsOfFocus = new List(); ClearAllItems(ref hmdPositions, ref hmdPositionsManipulated, ref distanceHmdPositionsManipulated, ref helpPointsOfFocus); } /// /// Master clear method for all tracking lists. /// /// HMD position list to clear /// Manipulated HMD position list to clear /// Distance list to clear /// Points of focus list to clear public static void ClearAllItems(ref List hmdPositions, ref List hmdPositionsManipulated, ref List distanceHmdPositionsManipulated, ref List pointsOfFocus) { hmdPositions.Clear(); hmdPositionsManipulated.Clear(); distanceHmdPositionsManipulated.Clear(); pointsOfFocus.Clear(); } /// /// Determines if movement qualifies as normal head movement (not gesture). /// /// Axis to check ("x", "y", or "z") /// List of HMD positions to analyze /// Minimum threshold for normal movement /// True if movement exceeds normal threshold public static bool NormalHeadMovement(string axis, List hmdPositions, double minNormalHeadMovements) { // calculate if the difference between the maximum and minimum value of a list of hmdPositions is greater than a specific threshold (minNormalHeadMovements) return (HmdPosition.Instance.GetMaxMinPosition("max", axis, hmdPositions).hmdPosition.Position.x - HmdPosition.Instance.GetMaxMinPosition("min", axis, hmdPositions).hmdPosition.Position.x) > minNormalHeadMovements; } /// /// Checks if enough time has passed for reliable gesture recognition. /// /// List of HMD positions /// Minimum time required for gesture detection /// Reference to application start flag /// Reference to time span reached flag public static void EnoughSamplesCollected(ref List hmdPositions, TimeSpan minTimeForGestures, ref bool startApplication, ref bool timeSpanGestureReached) { var helpHmdPositionsManipulated = new List(); var helpDistanceHmdPositionsManipulated = new List(); EnoughSamplesCollected(ref hmdPositions, ref helpHmdPositionsManipulated, ref helpDistanceHmdPositionsManipulated, minTimeForGestures, ref startApplication, ref timeSpanGestureReached); } /// /// Internal sample collection validation with list management. /// /// List of HMD positions /// Manipulated position list /// Distance calculation list /// Minimum time threshold /// Reference to start application flag /// Reference to time span flag private static void EnoughSamplesCollected(ref List hmdPositions, ref List hmdPositionsManipulated, ref List distanceHmdPositionsManipulated, TimeSpan minTimeForGestures, ref bool startApplication, ref bool timeSpanGestureReached) { //calculate if timespan between first and last entry is enough for recognizing possible shakes if ((hmdPositions[hmdPositions.Count - 1].Timestamp - hmdPositions[0].Timestamp) > minTimeForGestures) { //first time after appication hast started, therefore clear all Lists for a correct recognition if (startApplication) { ClearAllItems(ref hmdPositions, ref hmdPositionsManipulated, ref distanceHmdPositionsManipulated); startApplication = false; } timeSpanGestureReached = true; } else { timeSpanGestureReached = false; } } /// /// Finds all local maxima in a data series. /// /// Data series to analyze /// List of maximum peak values private static List FindMaxPeaks(IReadOnlyList list) { var peakList = new List(); for (var i = 1; i < (list.Count - 1); i++) { if ((list[i - 1] < list[i]) && (list[i] > list[i + 1])) peakList.Add(list[i]); } return peakList; } /// /// Finds all local minima in a data series. /// /// Data series to analyze /// List of minimum peak values private static List FindMinPeaks(IReadOnlyList list) { var peakList = new List(); for (var i = 1; i < (list.Count - 1); i++) { if ((list[i - 1] > list[i]) && (list[i] < list[i + 1])) peakList.Add(list[i]); } return peakList; } /// /// Converts HMD position list to float list for specified axis. /// /// List of HMD positions /// Axis to extract ("x", "y", or "z") /// List of float values for the specified axis private static List ConvertHmdPositionListIntoFloatList(List listHmdPositions, string axis) { var floatList = new List(); foreach (var pos in listHmdPositions) { switch (axis) { case "x": floatList.Add(pos.Position.x); break; case "y": floatList.Add(pos.Position.y); break; case "z": floatList.Add(pos.Position.z); break; } } return floatList; } /// /// Converts HMD position list to double list for specified axis. /// /// List of HMD positions /// Axis to extract ("x", "y", or "z") /// List of double values for the specified axis private static List ConvertHmdPositionListIntoDoubleList(List listHmdPositions, string axis) { var floatList = new List(); foreach (var pos in listHmdPositions) { switch (axis) { case "x": floatList.Add(pos.Position.x); break; case "y": floatList.Add(pos.Position.y); break; case "z": floatList.Add(pos.Position.z); break; } } return floatList; } /// /// Saves coordinate data to text file for research and parameter optimization. /// /// List of coordinate values to save /// Name identifier for the output file public static void SaveLine(List list, string listName) { var myFilePath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); using (var outputFile = new StreamWriter(Path.Combine(myFilePath, "Line_" + listName + ".txt"), true)) { foreach (double elem in list) outputFile.WriteLine(elem.ToString(CultureInfo.CurrentCulture)); } } } }