2021年6月3日木曜日

Vive Pro Eyeの視線EyeData_v2をCSVファイルに保存する

Vive Pro Eyeの視線データクラスEyeData_v2を丸ごとCSVファイルに保存する方法のメモ. JsonUtility.ToJsonを使えばクラスを丸ごとJsonデータに変換できるけど, 時系列データをそのままJson形式のデータを保存すると,毎フレーム同じようなラベル付きのテキスト情報になって無駄に容量を消費するし,簡単にグラフとか作れた方が嬉しい.だからCSVに変換し,ラベルはヘッダに加えてデータ量を削減する. Jsonデータから,列方向に全ラベルに対応したデータが並び,行方向に各フレームのデータが並ぶCSVファイルにする作戦. Json形式のデータは階層構造になっているのでそれを全部一列にならべる必要がある.

クラスをシリアライズする

まず,保存するデータをシリアライズする必要がある. これについては直接保存したいデータ型(クラス)の前に[Serializable]を追加するとできる. 逆に追加しなければ,そのJson化されないで無視される. 必要な変更箇所は以下の通り.

Assets\ViveSR\Scripts\Eye\SRanipal_EyeDataType_v2.cs

[StructLayout(LayoutKind.Sequential)]
[Serializable]
public struct SingleEyeExpression
{
[StructLayout(LayoutKind.Sequential)]
[Serializable]
public struct EyeExpression
{

Assets\ViveSR\Scripts\Eye\SRanipal_EyeData.cs

[StructLayout(LayoutKind.Sequential)]
/** @struct VerboseData
* A struct containing all data listed below.
*/
[Serializable]
public struct VerboseData
{
[StructLayout(LayoutKind.Sequential)]
[Serializable]
public struct EyeExpression
{

Json形式でクラスのデータ取得する

JsonUtility.ToJson()関数で任意のオブジェクトがJson形式のテキストに変換される. あとは,そのテキストをCSVに変換するだけ. 変換方法は, 「C#で階層構造をもつJSONとCSVを相互変換する」と「C#のオブジェクト配列をCSVに保存する/CSVから読み込む」にまとめられている. ヘッダは1回,データ行は繰り返し呼ばれる位置に配置する.

EyeCallback関数内で
private static void EyeCallback(ref EyeData_v2 eye_data)
{
    eyeData = eye_data.verbose_data.ShallowCopy();
}
-->

FixedUpdate関数内で

//オブジェクトをJSONに変換
string json_txt = JsonUtility.ToJson(eyeData);

//JSONからCSVのヘッダに変換
if(!has_header_written){
    string csv_header = JsonCsv.JsonToCsvHeader(json_txt);
    has_header_written = true;
    csv_results.Write(csv_header);
}

//JSONからCSVのデータ1行に変換
string csv_row = JsonCsv.JsonToCsvRow(json_txt);
csv_results.Write(csv_row);

Jsonデータの例

得られたJsonデータは以下のとおり. これで1フレーム分のデータ.毎回このデータが得られるとするとラベルが冗長であることが分かる.

{"left":{"eye_data_validata_bit_mask":31,"gaze_origin_mm":{"x":33.20198059082031,"y":2.810272216796875,"z":-34.43475341796875},"gaze_direction_normalized":{"x":0.0227203369140625,"y":-0.105804443359375,"z":0.9941253662109375},"pupil_diameter_mm":3.914947509765625,"eye_openness":1.0,"pupil_position_in_sensor_area":{"x":0.4758981466293335,"y":0.5303041338920593}},"right":{"eye_data_validata_bit_mask":31,"gaze_origin_mm":{"x":-27.541336059570314,"y":2.7649688720703127,"z":-32.7015380859375},"gaze_direction_normalized":{"x":0.16375732421875,"y":-0.0695648193359375,"z":0.984039306640625},"pupil_diameter_mm":3.9882049560546877,"eye_openness":1.0,"pupil_position_in_sensor_area":{"x":0.35452643036842348,"y":0.5137556195259094}},"combined":{"eye_data":{"eye_data_validata_bit_mask":3,"gaze_origin_mm":{"x":27.937347412109376,"y":2.8063507080078127,"z":-34.28453063964844},"gaze_direction_normalized":{"x":0.03497314453125,"y":-0.102752685546875,"z":0.99407958984375},"pupil_diameter_mm":0.0,"eye_openness":0.0,"pupil_position_in_sensor_area":{"x":0.0,"y":0.0}},"convergence_distance_validity":false,"convergence_distance_mm":0.0}}

CSV形式に変換する

Start関数内で

csv_results = new StreamWriter("results/eyedata.csv", false);   //true=追記 false=上書き
SRanipal_Eye.WrapperRegisterEyeDataCallback(
	Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic)EyeCallback));

FixedUpdate関数内で

Json形式をCSVに変換する方法は以下の記事に記載している.

//JSONからCSVのヘッダに変換
if(!has_header_written){
    string csv_header = JsonCsv.JsonToCsvHeader(json_txt);
    has_header_written = true;
    csv_results.Write(csv_header);
}

//JSONからCSVのデータ1行に変換
string csv_row = JsonCsv.JsonToCsvRow(json_txt);
csv_results.Write(csv_row);

これがCSVのヘッダ.ラベルに階層構造が組み込まれていることが分かる.

#left:eye_data_validata_bit_mask,left:gaze_origin_mm:x,left:gaze_origin_mm:y,left:gaze_origin_mm:z,left:gaze_direction_normalized:x,left:gaze_direction_normalized:y,left:gaze_direction_normalized:z,left:pupil_diameter_mm,left:eye_openness,left:pupil_position_in_sensor_area:x,left:pupil_position_in_sensor_area:y,right:eye_data_validata_bit_mask,right:gaze_origin_mm:x,right:gaze_origin_mm:y,right:gaze_origin_mm:z,right:gaze_direction_normalized:x,right:gaze_direction_normalized:y,right:gaze_direction_normalized:z,right:pupil_diameter_mm,right:eye_openness,right:pupil_position_in_sensor_area:x,right:pupil_position_in_sensor_area:y,combined:eye_data:eye_data_validata_bit_mask,combined:eye_data:gaze_origin_mm:x,combined:eye_data:gaze_origin_mm:y,combined:eye_data:gaze_origin_mm:z,combined:eye_data:gaze_direction_normalized:x,combined:eye_data:gaze_direction_normalized:y,combined:eye_data:gaze_direction_normalized:z,combined:eye_data:pupil_diameter_mm,combined:eye_data:eye_openness,combined:eye_data:pupil_position_in_sensor_area:x,combined:eye_data:pupil_position_in_sensor_area:y,combined:convergence_distance_validity,combined:convergence_distance_mm

これが各行のデータ.ラベルがなくてJson形式よりもすっきりしていることが分かる.

31,35.2647705078125,-4.49447631835938,-36.8798828125,0.05670166015625,-0.134231567382813,0.98931884765625,4.77302551269531,0,0.383619159460068,0.842255115509033,31,-25.6363677978516,-4.01924133300781,-36.5369415283203,0.105133056640625,-0.085845947265625,0.990737915039063,4.71017456054688,0,0.272316724061966,0.824171006679535,3,-25.6363677978516,-4.01924133300781,-36.5369415283203,0.105133056640625,-0.085845947265625,0.990737915039063,0,0,0,0,False,0

テキストデータでは分かりにくいのでCSVファイルを Googleスプレッドシートに変換したものをアップロードした. 「グラフ」タブには「left:gaze_direction_normalized:x」と「left:gaze_direction_normalized:y」の2つの列をx軸とy軸のデータとして折れ線グラフがプロットされている. これはC#上では,変数VerboseData::SingleEyeData::Vector3::floatクラスのオブジェクト eyeData.left.gaze_direction_normalized.xとeyeData.left.gaze_direction_normalized.yに対応している.

参考

https://docs.microsoft.com/ja-jp/dotnet/api/system.type.getfields?view=net-5.0 https://github.com/sinbad/UnityCsvUtil/blob/master/CsvUtil.cs https://gist.github.com/darktable/1411710#file-minijson-cs