immersivecognition / unity-experiment-framework

UXF - Framework for creating human behaviour experiments in Unity
https://immersivecognition.github.io/unity-experiment-framework/
MIT License
215 stars 41 forks source link

Tracking system: Is it possible to submit more than 1 row at a time? #91

Closed JAQuent closed 2 years ago

JAQuent commented 2 years ago

I've been working on a tracking system for what is currently on the screen by shooting rays from the camera and that all works fine. My aim is to detect whether or not a number of rays are hitting objects and if they do what object they hit.

My code works fine if I only have one ray but it doesn't when I use multiple rays because want to submit the result for each ray as a new row.

I really want to avoid having one tracker for each ray or even a new colum for each ray so I am wondering whether something like this is possible?

The problem with having a colum for each ray is that I wouldn't know how to header etc. Could that be done in for loop?

When I provide more than one ray I get the following error arises form the for loop in GetCurrentValues():

InvalidOperationException: The row does not contain values for the same columns as the columns in the table!
Table: time, rayIndex, x, y, objectDetected
Row: rayIndex, x, y, objectDetected, rayIndex, x, y, objectDetected, time
UXF.UXFDataTable.AddCompleteRow (UXF.UXFDataRow newRow) (at Assets/UXF/Scripts/Etc/UXFDataTable.cs:109)

Here is my script:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UXF;

public class screenTracker : Tracker {

    // Public vars
    public Camera cam;
    public List<float> x  = new List<float>();
    public List<float> y  = new List<float>();
    public int numRays;
    public bool showDebugRays = true;
    public string noObjectString = "NA";

    // Private vars
    public List<string> objectDetected = new List<string>();

    // // Start calc
    // void Start(){
    //     numRays = y.Count;
    // }

    // Set up header
    protected override void SetupDescriptorAndHeader(){
        measurementDescriptor = "objectsOnScreenTracker";

        customHeader = new string[]
        {
            "rayIndex",
            "x",
            "y",
            "objectDetected"
        };
    }

    // Get values
    protected override UXFDataRow GetCurrentValues(){

        objectDetected = ray2detectObjects(x, y, cam);

        var values = new UXFDataRow();

        // Write rows in a loop
        for(int i = 0; i < numRays; i++){
            values.Add(("rayIndex", i));
            values.Add(("x", x[i]));
            values.Add(("y", y[i]));
            values.Add(("objectDetected", objectDetected[i]));
        }
        return values;
    }

    // Function to detect objects on screen by rays
    List<string> ray2detectObjects(List<float> x, List<float> y, Camera cam){
        // Get number of rays
        numRays = y.Count;

        // Create var
        List<string> nameOfObjects = new List<string>();
        List<Ray> rays = new List<Ray>();

        for (int i = 0; i < numRays; i++){
            // Cast the ray and add to list
            Ray ray = cam.ViewportPointToRay(new Vector3(x[i], y[i], 0));
            rays.Add (ray);

            if(showDebugRays){
                Debug.DrawRay(rays[i].origin, rays[i].direction * 50, Color.red);
            }
            RaycastHit hit1;

            if (Physics.Raycast(rays[i], out hit1)){
                if(showDebugRays){
                    Debug.DrawRay(rays[i].origin, rays[i].direction * 50, Color.green);
                }
                print("I'm looking at " + hit1.transform.name + " with ray " + i);
                nameOfObjects.Add(hit1.transform.name);

            } else {
                nameOfObjects.Add(noObjectString);
            }
        }
        return nameOfObjects;
    }

}
jackbrookes commented 2 years ago

The way you are trying it will not work - you are only creating a single row, just adding duplicate items to that row. There are a few ways to do what you want to do.

First, you need to set the Update Type field in the inspector to Manual. That way you can manually record multiple rows per frame.

I think the best way would to record multiple lines per frame is with a coroutine. I modified your script, now it calls RecordRow manually multiple times per frame. The for loop just stores a row each iteration, then GetCurrentValues just grabs that stored row. GetCurrentValues is called internally within RecordRow.

There may be a better way if I change UXF internally but this should work for now.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UXF;

public class screenTracker : Tracker {

    // Public vars
    public Camera cam;
    public List<float> x  = new List<float>();
    public List<float> y  = new List<float>();
    public int numRays;
    public bool showDebugRays = true;
    public string noObjectString = "NA";

    // Private vars
    public List<string> objectDetected = new List<string>();

    private UXFDataRow currentRow;

    // Start calc
    void Start(){
        StartCoroutine(RecordRoutine());
    }

    IEnumerator RecordRoutine()
    {
        while (true)
        {
            if (recording)
            {
                objectDetected = ray2detectObjects(x, y, cam);
                for(int i = 0; i < numRays; i++){
                    var values = new UXFDataRow();
                    values.Add(("rayIndex", i));
                    values.Add(("x", x[i]));
                    values.Add(("y", y[i]));
                    values.Add(("objectDetected", objectDetected[i]));
                    currentRow = values;
                    RecordRow(); // record for each ray
                    currentRow = null;
                }
            }
            yield return null; // wait until next frame
        }
    }

    // Set up header
    protected override void SetupDescriptorAndHeader(){
        measurementDescriptor = "objectsOnScreenTracker";

        customHeader = new string[]
        {
            "rayIndex",
            "x",
            "y",
            "objectDetected"
        };
    }

    // Get values
    protected override UXFDataRow GetCurrentValues(){
        return currentRow;
    }

    // Function to detect objects on screen by rays
    List<string> ray2detectObjects(List<float> x, List<float> y, Camera cam){
        // Get number of rays
        numRays = y.Count;

        // Create var
        List<string> nameOfObjects = new List<string>();
        List<Ray> rays = new List<Ray>();

        for (int i = 0; i < numRays; i++){
            // Cast the ray and add to list
            Ray ray = cam.ViewportPointToRay(new Vector3(x[i], y[i], 0));
            rays.Add (ray);

            if(showDebugRays){
                Debug.DrawRay(rays[i].origin, rays[i].direction * 50, Color.red);
            }
            RaycastHit hit1;

            if (Physics.Raycast(rays[i], out hit1)){
                if(showDebugRays){
                    Debug.DrawRay(rays[i].origin, rays[i].direction * 50, Color.green);
                }
                print("I'm looking at " + hit1.transform.name + " with ray " + i);
                nameOfObjects.Add(hit1.transform.name);

            } else {
                nameOfObjects.Add(noObjectString);
            }
        }
        return nameOfObjects;
    }

}
JAQuent commented 2 years ago

Unfortunately with the new version of UXF this is not wroking any more. I get the following messages:

image

I'd also love make this general & versatile enough to be used by other people. The basic idea is that you supply a bunch of ray coordiantes and during the trial each time the ray is hitting an object it is recorded. After my preliminary testing, I think this could be useful (e.g. for recording when an object was first on the screen, where it was etc.). If you think it worth developing further should I use this issue or open another so it can be developed as a feature?

Current Code

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UXF;
using System.Linq;

public class screenTracker : Tracker {
    // Public vars
    public Camera cam;
    public List<float> x  = new List<float>();
    public List<float> y  = new List<float>();
    public int numRays;
    public bool debugMode = true; // Prints log entry and shows debug rays
    public string noObjectString = "NA";
    public bool saveNoObject = false;

    // Private vars
    public List<string> objectDetected = new List<string>();
    private UXFDataRow currentRow;
    private bool recording = false;

    // Start calc
    void Start(){
        // Read .txt file
        string[] rayCoordinates = readText("input_data/rayCoordinates.txt");

        // Parsing information for rays
        int indexCount = 2; // because there are 7 variables with 1 column for each variable
        int n = rayCoordinates.Length/indexCount;

        // First var
        int indexMin = 0;
        x     = parseFloatListFromString(indexMin, indexCount, rayCoordinates);

        // Second var
        indexMin = 1;
        y     = parseFloatListFromString(indexMin, indexCount, rayCoordinates);

        // Start the recoding
        StartCoroutine(RecordRoutine());
    }

    // Start and stop recoding functions to be added to UXF.rig Events (Start and End of trial)
    void StartRecording(){
        recording = true;
    }

    // See above
    void StopRecording(){
        recording = false;
    }

    IEnumerator RecordRoutine(){
        while (true){
            if (recording){
                objectDetected = ray2detectObjects(x, y, cam);
                for(int i = 0; i < numRays; i++){
                    // When no object was detected save only if saveNoObject is true
                    if(objectDetected[i] == noObjectString){
                        if(saveNoObject){
                            var values = new UXFDataRow();
                            values.Add(("rayIndex", i));
                            values.Add(("x", x[i]));
                            values.Add(("y", y[i]));
                            values.Add(("objectDetected", objectDetected[i]));
                            currentRow = values;
                            RecordRow(); // record for each ray
                            currentRow = null;
                        }
                    } else {
                            var values = new UXFDataRow();
                            values.Add(("rayIndex", i));
                            values.Add(("x", x[i]));
                            values.Add(("y", y[i]));
                            values.Add(("objectDetected", objectDetected[i]));
                            currentRow = values;
                            RecordRow(); // record for each ray
                            currentRow = null;
                    }
                }
            }
            yield return null; // wait until next frame
        }
    }

    // Set up header
    protected override void SetupDescriptorAndHeader(){
        measurementDescriptor = "objectsOnScreenTracker";

        customHeader = new string[]{
            "rayIndex",
            "x",
            "y",
            "objectDetected"
        };
    }

    // Get values
    protected override UXFDataRow GetCurrentValues(){
        return currentRow;
    }

    string[] readText(string fileName){
        // Loads .txt file and split by \t and \n
        string inputText = System.IO.File.ReadAllText(fileName);
        string[] stringList = inputText.Split('\t', '\n'); //splits by tabs and lines
        return stringList;
    }

    // Parsing informatiom from text file converting to list of floats
    List<float> parseFloatListFromString(int indexMin, int indexCount, string[] stringList){
        // Selects items, converts strings to floats and then creates a list for floats
        int indexMax = stringList.Length - indexCount + indexMin;
        var index = Enumerable.Repeat(indexMin, (int)((indexMax - indexMin) / indexCount) + 1).Select((tr, ti) => tr + (indexCount * ti)).ToList();
        List<float> floatList = index.Select(x => float.Parse(stringList[x])).ToList();
        return floatList;
    }

    // Function to detect objects on screen by rays
    List<string> ray2detectObjects(List<float> x, List<float> y, Camera cam){
        // Get number of rays
        numRays = y.Count;

        // Create var
        List<string> nameOfObjects = new List<string>();

        for (int i = 0; i < numRays; i++){
            // Cast the ray and add to list
            Ray ray = cam.ViewportPointToRay(new Vector3(x[i], y[i], 0));

            // Display ray for debugging
            if(debugMode){
                Debug.DrawRay(ray.origin, ray.direction * 50, Color.red);
            }

            // Raycast and check if something is hit
            RaycastHit hit1;
            if (Physics.Raycast(ray, out hit1)){
                if(debugMode){
                    Debug.DrawRay(ray.origin, ray.direction * 50, Color.green);
                    print("I'm looking at " + hit1.transform.name + " with ray " + i);
                }

                // Add name of GameObject that was hit
                nameOfObjects.Add(hit1.transform.name);
            } else {
                // Add noObjectString becuase no object was hit by ray
                nameOfObjects.Add(noObjectString);
            }
        }
        return nameOfObjects;
    }

}
jackbrookes commented 2 years ago

Hi, yes I recently updated UXF Tracker implementation. I knew this would break things, but it was needed to fix a critical bug, and is better for the future. It should not be difficult to adapt the code. See the Custom Tracker section on the Wiki.

To develop as a general purpose feature it looks a little complex, and the coding style would need to be changed a lot to fit within UXF that is maintained. You can upload as a Gist and I can link this in the wiki. But as always feel free to make a pull request if you know how, but it may be a while before it is considered to be added to UXF (no time for active development at the moment)