Unity에서 로봇 시뮬레이션을 수행하면서 중요한 데이터 수집 과정을 '데이터 로깅'이라고 한다. 이 과정은 다양한 센서나 물리적 인터랙션의 결과값들을 시간에 따라 저장하여 나중에 분석하거나 디버깅하는 데 유용하게 사용할 수 있다. 특히 로봇 시뮬레이션에서 로깅된 데이터는 시스템의 상태, 센서 값, 제어 명령어 등을 포함하여 다양한 분석을 할 수 있게 해준다.

데이터 로깅의 목적

로깅은 주로 다음과 같은 목적을 위해 수행된다: 1. 시스템 성능 분석: 주행 경로, 로봇의 위치, 속도, 가속도 등을 기록하여 시뮬레이션 성능을 평가할 수 있다. 2. 센서 데이터 저장: 라이다, IMU, 카메라 등에서 발생한 센서 데이터를 로깅하여 이후의 처리 및 분석에서 사용할 수 있다. 3. 디버깅: 시스템 오류가 발생할 경우, 문제의 원인을 파악하기 위해 과거 데이터를 분석할 수 있다.

Unity에서 데이터 로깅 구현하기

Unity에서 데이터를 로깅하는 기본적인 방법은 C# 스크립트를 통해 원하는 데이터를 파일에 저장하는 방식이다. 주로 사용되는 파일 포맷은 CSV(Comma-Separated Values) 파일이다. CSV 파일은 각 시간 스텝마다 데이터 값을 저장하기에 적합하며, 후속 분석에서도 활용하기 용이한다.

using System.IO;

public class DataLogger : MonoBehaviour
{
    private StreamWriter writer;

    void Start()
    {
        writer = new StreamWriter("simulation_data.csv");
        writer.WriteLine("Time,PositionX,PositionY,PositionZ,VelocityX,VelocityY,VelocityZ");
    }

    void Update()
    {
        Vector3 position = transform.position;
        Vector3 velocity = GetComponent<Rigidbody>().velocity;
        string data = Time.time + "," + position.x + "," + position.y + "," + position.z + "," +
                      velocity.x + "," + velocity.y + "," + velocity.z;
        writer.WriteLine(data);
    }

    void OnApplicationQuit()
    {
        writer.Close();
    }
}

이 예제 코드에서 다음과 같은 데이터를 기록한다: - 시간 t - 로봇의 위치 \mathbf{p}(t) = [p_x(t), p_y(t), p_z(t)]^T - 로봇의 속도 \mathbf{v}(t) = [v_x(t), v_y(t), v_z(t)]^T

데이터는 CSV 파일에 각각의 시간 스텝마다 기록되며, 시뮬레이션이 종료될 때 파일이 닫힌다.

시간 스텝에 따른 데이터 로깅

Unity에서 데이터 로깅의 핵심은 시간 스텝에 따라 데이터를 기록하는 것이다. Unity의 프레임워크에서 Update() 함수는 매 프레임마다 호출되며, 이를 통해 매 프레임의 데이터를 저장할 수 있다. 그러나 모든 프레임에서 데이터를 로깅하면 과도한 데이터가 생성될 수 있으므로, 특정 주기마다 데이터를 로깅하는 방식도 고려해야 한다.

이때 로깅 주기를 다음과 같이 설정할 수 있다:

\Delta t = \frac{1}{f_{\text{log}}}

여기서 \Delta t는 로깅 간격이고, f_{\text{log}}는 로깅 주파수(Hz)이다. 예를 들어, 10 Hz 주기로 데이터를 기록하려면, 0.1초마다 데이터 로깅이 이루어져야 한다.

이를 구현하기 위한 방법은 다음과 같다.

public class TimedDataLogger : MonoBehaviour
{
    private StreamWriter writer;
    public float logFrequency = 10.0f;
    private float nextLogTime = 0.0f;

    void Start()
    {
        writer = new StreamWriter("timed_simulation_data.csv");
        writer.WriteLine("Time,PositionX,PositionY,PositionZ,VelocityX,VelocityY,VelocityZ");
        nextLogTime = Time.time;
    }

    void Update()
    {
        if (Time.time >= nextLogTime)
        {
            Vector3 position = transform.position;
            Vector3 velocity = GetComponent<Rigidbody>().velocity;
            string data = Time.time + "," + position.x + "," + position.y + "," + position.z + "," +
                          velocity.x + "," + velocity.y + "," + velocity.z;
            writer.WriteLine(data);

            nextLogTime += 1.0f / logFrequency;
        }
    }

    void OnApplicationQuit()
    {
        writer.Close();
    }
}

위 코드에서 주기적인 로깅을 위해 f_{\text{log}} = 10으로 설정된 로그 주파수를 사용한다. 실제 데이터는 지정된 주기 \Delta t = 0.1마다 기록된다.

다양한 데이터 유형의 로깅

Unity에서 로깅할 수 있는 데이터는 로봇의 위치나 속도뿐만 아니라, 센서 데이터, 로봇의 상태, 외부 환경 정보 등 매우 다양한다. 다양한 유형의 데이터를 로깅하기 위해서는 데이터 형식에 맞는 처리가 필요하며, 데이터 저장 방식도 그에 따라 달라진다.

1. 센서 데이터 로깅

센서 데이터는 일반적으로 높은 주파수로 발생하며, 라이다(LiDAR), 카메라, IMU 등의 다양한 센서로부터 얻을 수 있다. 각 센서의 특성에 맞는 데이터 구조를 로깅해야 하며, 각 센서에서 발생하는 노이즈를 포함하여 시뮬레이션 데이터를 기록하는 것이 중요하다.

예를 들어, IMU(Inertial Measurement Unit) 센서에서 나오는 데이터를 로깅한다고 가정하면, 가속도 및 각속도 데이터를 수집할 수 있다. IMU는 다음과 같은 데이터를 생성한다: - 가속도: \mathbf{a}(t) = [a_x(t), a_y(t), a_z(t)]^T - 각속도: \boldsymbol{\omega}(t) = [\omega_x(t), \omega_y(t), \omega_z(t)]^T

이를 기록하기 위한 예시는 다음과 같다.

public class IMUDataLogger : MonoBehaviour
{
    private StreamWriter writer;

    void Start()
    {
        writer = new StreamWriter("imu_data.csv");
        writer.WriteLine("Time,AccelX,AccelY,AccelZ,GyroX,GyroY,GyroZ");
    }

    void Update()
    {
        Vector3 accel = GetComponent<YourIMUSensor>().GetAcceleration();
        Vector3 gyro = GetComponent<YourIMUSensor>().GetGyroscope();

        string data = Time.time + "," + accel.x + "," + accel.y + "," + accel.z + "," +
                      gyro.x + "," + gyro.y + "," + gyro.z;
        writer.WriteLine(data);
    }

    void OnApplicationQuit()
    {
        writer.Close();
    }
}

이 스크립트는 IMU 센서에서 얻은 가속도와 각속도 데이터를 시간에 따라 로깅하며, IMU 데이터를 기록하기 위한 포맷을 정의한다. 기록된 데이터는 다음과 같은 구조를 갖는다:

\text{Time}, a_x, a_y, a_z, \omega_x, \omega_y, \omega_z

2. 로봇 상태 데이터 로깅

로봇의 상태 데이터를 로깅하는 것도 중요한 작업 중 하나이다. 로봇의 상태는 주로 로봇 위치, 속도, 회전 행렬 등을 포함한다. 로봇의 회전은 쿼터니언(Quaternion) 또는 오일러 각(Euler angles)로 표현될 수 있으며, 이 정보를 로그 파일에 저장할 수 있다.

오일러 각은 \mathbf{\theta}(t) = [\theta_x(t), \theta_y(t), \theta_z(t)]^T로 표현되며, 로봇의 회전 상태를 기록할 수 있다.

다음 예제는 로봇의 위치와 오일러 각을 로깅하는 방법을 보여준다.

public class RobotStateLogger : MonoBehaviour
{
    private StreamWriter writer;

    void Start()
    {
        writer = new StreamWriter("robot_state.csv");
        writer.WriteLine("Time,PosX,PosY,PosZ,Roll,Pitch,Yaw");
    }

    void Update()
    {
        Vector3 position = transform.position;
        Vector3 rotation = transform.eulerAngles; // 오일러 각

        string data = Time.time + "," + position.x + "," + position.y + "," + position.z + "," +
                      rotation.x + "," + rotation.y + "," + rotation.z;
        writer.WriteLine(data);
    }

    void OnApplicationQuit()
    {
        writer.Close();
    }
}

여기서 오일러 각을 사용해 로봇의 회전 상태를 기록하며, 로그 데이터는 다음과 같은 형식으로 기록된다:

\text{Time}, p_x, p_y, p_z, \theta_x, \theta_y, \theta_z

3. 다차원 데이터의 로깅

때로는 센서 데이터나 로봇의 상태가 다차원 배열 형태일 수 있다. 예를 들어, 라이다(LiDAR) 센서의 경우, 각 스캔 포인트는 특정 방향에서의 거리 값을 나타내며, 이 값들은 배열 형태로 저장된다. 이러한 경우, 배열 데이터를 기록하는 방법은 CSV 파일 형식에 맞게 데이터를 직렬화하여 저장하는 방식이 필요하다.

public class LidarDataLogger : MonoBehaviour
{
    private StreamWriter writer;

    void Start()
    {
        writer = new StreamWriter("lidar_data.csv");
        writer.WriteLine("Time,ScanData");
    }

    void Update()
    {
        float[] scanData = GetComponent<LidarSensor>().GetScan();

        string scanDataString = string.Join(",", scanData);
        string data = Time.time + "," + scanDataString;
        writer.WriteLine(data);
    }

    void OnApplicationQuit()
    {
        writer.Close();
    }
}

이 코드는 라이다(LiDAR) 센서에서 얻은 다차원 데이터를 시간에 따라 기록하며, 각 스캔 데이터가 CSV 파일에 직렬화된다.

파일 저장 형식

Unity에서 데이터를 로깅할 때는 주로 CSV 파일 형식을 사용하지만, 상황에 따라 다른 파일 형식을 사용할 수도 있다. 여러 형식 중에서 데이터를 효과적으로 저장하고 나중에 처리할 수 있는 몇 가지 형식을 소개한다.

1. CSV (Comma-Separated Values)

CSV 파일은 가장 널리 사용되는 형식으로, 각 데이터 포인트를 쉼표로 구분하여 기록한다. CSV 파일은 텍스트 기반이므로 사람이 읽기 쉬우며, 다양한 소프트웨어에서 처리 가능한다. 하지만 이진 데이터복잡한 구조를 저장하는 데는 적합하지 않는다.

CSV 파일의 장점: - 텍스트 파일로 간단히 작성 가능 - 다양한 데이터 분석 도구와 호환 가능 (예: Excel, Pandas)

CSV 파일의 단점: - 대용량 데이터를 다룰 때 비효율적 (텍스트 파일이므로 크기가 큼) - 이진 데이터나 복잡한 배열을 저장할 때 불편함

2. JSON (JavaScript Object Notation)

JSON 파일 형식은 객체 구조를 표현하는 데 유용하다. 배열, 중첩된 데이터, 이진 데이터 등 복잡한 데이터 구조를 효율적으로 저장할 수 있다. JSON 형식은 특히 웹 기반의 데이터 처리네트워크 전송에 유용하며, API와의 연동에서도 자주 사용된다.

using System.IO;
using UnityEngine;
using System.Collections.Generic;

public class JsonDataLogger : MonoBehaviour
{
    private List<LogData> logDataList = new List<LogData>();

    [System.Serializable]
    public class LogData
    {
        public float time;
        public Vector3 position;
        public Vector3 velocity;
    }

    void Update()
    {
        LogData logData = new LogData();
        logData.time = Time.time;
        logData.position = transform.position;
        logData.velocity = GetComponent<Rigidbody>().velocity;
        logDataList.Add(logData);
    }

    void OnApplicationQuit()
    {
        string json = JsonUtility.ToJson(new { logs = logDataList }, true);
        File.WriteAllText("simulation_data.json", json);
    }
}

이 코드에서는 JSON 형식으로 데이터를 저장하여 각 시간의 위치 및 속도를 로깅한다. JSON은 다음과 같은 형식으로 기록된다:

{
  "logs": [
    {
      "time": 0.0,
      "position": { "x": 0.0, "y": 1.0, "z": 0.0 },
      "velocity": { "x": 0.0, "y": 0.0, "z": 0.0 }
    },
    {
      "time": 0.1,
      "position": { "x": 0.1, "y": 1.1, "z": 0.1 },
      "velocity": { "x": 0.2, "y": 0.1, "z": 0.2 }
    }
  ]
}

JSON 파일의 장점: - 객체 구조를 효과적으로 표현 가능 - 네트워크 전송 및 웹 기반 시스템과 호환성 높음

JSON 파일의 단점: - 텍스트 기반이므로 대용량 데이터에 비효율적 - 바이너리 데이터를 다루기 어려움

3. 이진 파일 (Binary Files)

이진 파일 형식은 대용량 데이터를 저장할 때 적합한다. 텍스트 파일보다 크기가 작고 빠르게 읽고 쓰기가 가능한다. 이진 파일은 CSV나 JSON보다 덜 직관적일 수 있지만, 고성능이 요구되는 로깅 시스템에서 자주 사용된다.

using System.IO;
using UnityEngine;

public class BinaryDataLogger : MonoBehaviour
{
    private BinaryWriter writer;

    void Start()
    {
        writer = new BinaryWriter(File.Open("simulation_data.bin", FileMode.Create));
    }

    void Update()
    {
        writer.Write(Time.time);
        writer.Write(transform.position.x);
        writer.Write(transform.position.y);
        writer.Write(transform.position.z);
        writer.Write(GetComponent<Rigidbody>().velocity.x);
        writer.Write(GetComponent<Rigidbody>().velocity.y);
        writer.Write(GetComponent<Rigidbody>().velocity.z);
    }

    void OnApplicationQuit()
    {
        writer.Close();
    }
}

이 코드에서는 이진 데이터로 시간, 위치, 속도 정보를 기록한다. 이진 파일은 매우 효율적이므로 대량의 실시간 데이터를 처리할 때 유리한다.

이진 파일의 장점: - 매우 빠른 읽기/쓰기 속도 - 파일 크기 최소화

이진 파일의 단점: - 사람이 읽기 어려움 (텍스트가 아님) - 분석 도구로 바로 처리하기 어려움

로그 데이터의 동기화 문제

Unity에서 로깅 시 센서 데이터로봇 상태는 서로 다른 주기로 발생할 수 있다. 예를 들어, IMU 센서는 100 Hz로 데이터를 생성하는 반면, 라이다는 10 Hz로 데이터를 생성할 수 있다. 이런 경우 동기화(synchronization)가 중요하다.

동기화 문제를 해결하기 위해, 각 센서 데이터를 기록할 때 타임스탬프를 함께 기록하고, 나중에 데이터 분석 시 동일한 시간 축에서 비교할 수 있도록 데이터를 정렬해야 한다. 각 데이터 포인트에 타임스탬프를 추가하면 다른 주기로 발생한 데이터를 정렬하여 분석할 수 있다.

타임스탬프를 활용한 데이터 동기화

각각의 센서나 로봇 상태 데이터를 동기화하는 가장 효과적인 방법은 각 데이터 포인트에 타임스탬프를 추가하는 것이다. 타임스탬프는 각 데이터가 생성된 시점을 나타내며, 서로 다른 주기로 발생한 데이터를 나중에 동일한 시간 축에서 비교할 수 있도록 도와준다.

다음과 같은 방식으로 데이터를 기록할 때 타임스탬프를 포함하여 동기화를 수행할 수 있다.

1. 타임스탬프 추가

각 데이터가 기록될 때마다 해당 데이터가 생성된 시점인 타임스탬프를 추가한다. Unity의 경우, Time.time을 사용하여 시뮬레이션 시작 후 경과 시간을 가져올 수 있으며, 이를 타임스탬프로 활용할 수 있다.

예를 들어, IMU 센서 데이터와 라이다 센서 데이터를 동시에 기록할 때 타임스탬프를 추가하는 코드를 작성해보겠다.

public class SensorDataLogger : MonoBehaviour
{
    private StreamWriter writer;

    void Start()
    {
        writer = new StreamWriter("sensor_data.csv");
        writer.WriteLine("Time,AccelX,AccelY,AccelZ,GyroX,GyroY,GyroZ,LidarScan");
    }

    void Update()
    {
        // IMU 데이터 로깅
        Vector3 accel = GetComponent<YourIMUSensor>().GetAcceleration();
        Vector3 gyro = GetComponent<YourIMUSensor>().GetGyroscope();

        // 라이다 데이터 로깅
        float[] lidarScan = GetComponent<LidarSensor>().GetScan();
        string lidarDataString = string.Join(",", lidarScan);

        // 타임스탬프 포함하여 데이터 기록
        string data = Time.time + "," + accel.x + "," + accel.y + "," + accel.z + "," +
                      gyro.x + "," + gyro.y + "," + gyro.z + "," + lidarDataString;
        writer.WriteLine(data);
    }

    void OnApplicationQuit()
    {
        writer.Close();
    }
}

위 코드에서는 IMU 센서 데이터라이다 센서 데이터가 타임스탬프와 함께 기록된다. 타임스탬프는 두 센서의 데이터가 서로 다른 주기로 발생하더라도, 나중에 데이터를 정렬하고 분석할 수 있도록 해준다.

2. 데이터 정렬 및 동기화

타임스탬프가 있는 데이터를 로깅한 후, 데이터를 동기화하려면 각 센서의 데이터가 발생한 시점을 기준으로 시간 축에 맞추어 데이터를 정렬해야 한다. 이를 위해, 각 데이터 포인트는 타임스탬프를 기준으로 정렬될 수 있으며, 필요한 경우 보간(interpolation)을 통해 동일한 시간 축에 데이터를 맞출 수 있다.

예를 들어, IMU 데이터는 100 Hz로 발생하고, 라이다 데이터는 10 Hz로 발생한다고 가정하면, IMU 데이터가 라이다 데이터 사이에 존재할 수 있다. 이를 해결하기 위해, 시간 t에 따른 데이터를 보간하여 동일한 시간 축에서 데이터를 비교할 수 있다.

데이터를 보간하는 방법으로 선형 보간(linear interpolation)이 가장 일반적으로 사용된다. 선형 보간은 두 데이터 포인트 사이의 값을 다음과 같이 계산한다:

y(t) = y_1 + \frac{(t - t_1)}{(t_2 - t_1)} \cdot (y_2 - y_1)

여기서: - t는 보간하려는 시간 - t_1, t_2는 인접한 두 데이터 포인트의 시간 - y_1, y_2는 인접한 두 데이터 포인트의 값

3. 동기화된 데이터의 분석

동기화된 데이터를 이용해 다양한 분석 작업을 수행할 수 있다. 예를 들어, 로봇의 경로 추적 성능을 평가하거나, 센서 데이터의 정확도를 분석할 수 있다. 동기화된 데이터를 활용하면, 라이다 센서에서 발생한 장애물 데이터를 IMU 데이터와 비교하여 로봇의 동작을 분석할 수 있다.

동기화된 데이터를 CSV 파일로 정리하면 다음과 같은 형식이 된다:

Time AccelX AccelY AccelZ GyroX GyroY GyroZ LidarScan
0.01 0.1 0.0 9.8 0.02 0.01 -0.01 10.5, 20.1, 15.3, ...
0.02 0.1 0.0 9.8 0.02 0.01 -0.01 10.7, 20.2, 15.5, ...
0.03 0.1 0.0 9.8 0.02 0.01 -0.01 11.0, 20.3, 15.6, ...

이처럼 타임스탬프가 있는 데이터를 통해 다양한 주기로 발생하는 데이터를 효과적으로 동기화할 수 있다.