Reading Native-Format Recordings#
In this tutorial, we demonstrate how to load a Neon recording in native format (stored on the companion device or downloaded as native data) and explore the data structure. We also illustrate PyNeon’s unified API, which handles both native and cloud formats seamlessly, and show how to convert native data to cloud format.
Downloading Sample Data#
We will use the same “simple” dataset as in the previous tutorial, and analyze the native format instead.
[1]:
from pyneon import Dataset, Recording, get_sample_data
# Download sample data (if not existing) and return the path
sample_dir = get_sample_data("simple")
native_dir = sample_dir / "Native Recording Data"
cloud_dir = sample_dir / "Timeseries Data + Scene Video"
print(native_dir)
C:\Users\qian.chu\Documents\GitHub\PyNeon\data\simple\Native Recording Data
A dataset in native format has the following directory structure:
[2]:
from seedir import seedir
seedir(native_dir)
Native Recording Data/
├─simple1-56fcec49/
│ ├─android.log.zip
│ ├─blinks ps1.raw
│ ├─blinks ps1.time
│ ├─blinks.dtype
│ ├─calibration.bin
│ ├─event.time
│ ├─event.txt
│ ├─extimu ps1.raw
│ ├─extimu ps1.time
│ ├─eye_state ps1.raw
│ ├─eye_state ps1.time
│ ├─eye_state.dtype
│ ├─fixations ps1.raw
│ ├─fixations ps1.time
│ ├─fixations.dtype
│ ├─gaze ps1.raw
│ ├─gaze ps1.time
│ ├─gaze.dtype
│ ├─gaze_200hz.raw
│ ├─gaze_200hz.time
│ ├─gaze_right ps1.raw
│ ├─gaze_right ps1.time
│ ├─imu ps1.raw
│ ├─imu ps1.time
│ ├─imu.dtype
│ ├─imu.proto
│ ├─info.json
│ ├─manifest.json
│ ├─manifest.json.crc
│ ├─Neon Scene Camera v1 ps1.mp4
│ ├─Neon Scene Camera v1 ps1.time
│ ├─Neon Scene Camera v1 ps1.time_aux
│ ├─Neon Sensor Module v1 ps1.mp4
│ ├─Neon Sensor Module v1 ps1.time
│ ├─Neon Sensor Module v1 ps1.time_aux
│ ├─Neon Sensor Module v1_sae_log_1.bin
│ ├─template.json
│ ├─wearer.json
│ ├─worn ps1.raw
│ ├─worn.dtype
│ └─worn_200hz.raw
└─simple2-6ca28606/
├─android.log.zip
├─blinks ps1.raw
├─blinks ps1.time
├─blinks.dtype
├─calibration.bin
├─event.time
├─event.txt
├─extimu ps1.raw
├─extimu ps1.time
├─eye_state ps1.raw
├─eye_state ps1.time
├─eye_state.dtype
├─fixations ps1.raw
├─fixations ps1.time
├─fixations.dtype
├─gaze ps1.raw
├─gaze ps1.time
├─gaze.dtype
├─gaze_200hz.raw
├─gaze_200hz.time
├─gaze_right ps1.raw
├─gaze_right ps1.time
├─imu ps1.raw
├─imu ps1.time
├─imu.dtype
├─imu.proto
├─info.json
├─manifest.json
├─manifest.json.crc
├─Neon Scene Camera v1 ps1.mp4
├─Neon Scene Camera v1 ps1.time
├─Neon Scene Camera v1 ps1.time_aux
├─Neon Sensor Module v1 ps1.mp4
├─Neon Sensor Module v1 ps1.time
├─Neon Sensor Module v1 ps1.time_aux
├─Neon Sensor Module v1_sae_log_1.bin
├─template.json
├─wearer.json
├─worn ps1.raw
├─worn.dtype
└─worn_200hz.raw
PyNeon provides a Dataset class to represent a collection of recordings. A dataset can contain one or more recordings. Here, we instantiate a Dataset by providing the path to the native format data directory.
[3]:
dataset = Dataset(native_dir)
print(dataset)
Dataset | 2 recordings
Dataset provides index-based access to its recordings through the recordings attribute, which contains a list of Recording instances. Individual recordings can be accessed by index:
[4]:
rec = dataset[0] # Internally accesses the recordings attribute
print(type(rec))
print(rec.recording_dir)
<class 'pyneon.recording.Recording'>
C:\Users\qian.chu\Documents\GitHub\PyNeon\data\simple\Native Recording Data\simple1-56fcec49
Alternatively, you can directly load a single Recording by specifying the recording’s folder path:
Recording Metadata and Data Access#
You can quickly obtain an overview of a Recording by printing the instance. This displays basic metadata (recording ID, wearer ID, recording start time, and duration) and the paths to available data files. Note that at this point, data files are located but not yet loaded into memory.
[5]:
print(rec)
Data format: native (version: 2.5)
Recording ID: 56fcec49-d660-4d67-b5ed-ba8a083a448a
Wearer ID: 028e4c69-f333-4751-af8c-84a09af079f5
Wearer name: Pilot
Recording start time: 2025-12-18 17:13:49.460000
Recording duration: 8235000000 ns (8.235 s)
Format-Agnostic API: Accessing Data#
One of PyNeon’s key strengths is its format-agnostic API. Whether your data is in native or cloud format, the same code works identically. This means you can write analysis pipelines that work seamlessly with either format. Below, we demonstrate accessing data from this native recording using the same approach as the cloud format tutorial.
Individual data streams can be accessed as properties of the Recording instance. For example, recording.gaze retrieves gaze data and loads it into memory. If you attempt to access unavailable data, PyNeon returns None and issues a warning message.
[6]:
# Gaze and fixation data are available
gaze = rec.gaze
print(gaze)
saccades = rec.saccades
print(saccades)
scene_video = rec.scene_video
print(scene_video)
Stream type: gaze
Number of samples: 1048
First timestamp: 1766074431275967547
Last timestamp: 1766074436535834547
Uniformly sampled: False
Duration: 5.26 seconds
Effective sampling frequency: 199.05 Hz
Nominal sampling frequency: 200 Hz
Columns: ['gaze x [px]', 'gaze y [px]', 'worn', 'azimuth [deg]', 'elevation [deg]']
Events type: saccades
Number of samples: 11
Columns: ['start timestamp [ns]', 'end timestamp [ns]', 'amplitude [px]', 'amplitude [deg]', 'mean velocity [px/s]', 'peak velocity [px/s]', 'duration [ms]']
Video name: Neon Scene Camera v1 ps1.mp4
Video height: 1200 px
Video width: 1600 px
Number of frames: 153
First timestamp: 1766074431584148547
Last timestamp: 1766074436631408547
Duration: 5.05 seconds
Effective FPS: 30.11
Note that accessing native data may trigger on-the-fly conversion from raw binary files (.raw, .time, .dtype) to DataFrames. PyNeon handles this transparently, so the resulting data structures are identical to cloud format data.
[7]:
print(gaze.data.head())
print(gaze.data.dtypes)
gaze x [px] gaze y [px] worn azimuth [deg] \
timestamp [ns]
1766074431275967547 731.885864 503.253845 -1 -4.384848
1766074431280967547 735.500916 502.152618 -1 -4.152129
1766074431285967547 735.843140 499.517426 -1 -4.130098
1766074431290967547 735.056641 502.690063 -1 -4.180729
1766074431295967547 736.322205 501.840668 -1 -4.099258
elevation [deg]
timestamp [ns]
1766074431275967547 6.207878
1766074431280967547 6.278540
1766074431285967547 6.447632
1766074431290967547 6.244054
1766074431295967547 6.298557
gaze x [px] float64
gaze y [px] float64
worn Int8
azimuth [deg] float64
elevation [deg] float64
dtype: object
Converting Native Data to Cloud Format#
A common workflow is to convert native format data to cloud format for easier sharing or integration with other tools. PyNeon provides the export_cloud_format() method to accomplish this seamlessly.
The conversion process:
Reads native binary files and converts them to CSV format
Preserves all data integrity and metadata
Outputs a standardized directory structure compatible with Pupil Cloud
Let’s export this recording to cloud format:
[8]:
from pathlib import Path
# Define output directory for cloud format data
export_dir = Path("./export")
# Export the native recording to cloud format
rec.export_cloud_format(export_dir, rebase=False)
print(f"Successfully exported to: {export_dir.resolve()}")
Successfully exported to: C:\Users\qian.chu\Documents\GitHub\PyNeon\source\tutorials\export
Let’s verify the exported cloud format directory structure:
[9]:
seedir(export_dir)
export/
├─3d_eye_states.csv
├─blinks.csv
├─events.csv
├─fixations.csv
├─gaze.csv
├─imu.csv
├─info.json
├─Neon Scene Camera v1 ps1.mp4
├─saccades.csv
├─scene_camera.json
├─template.csv
└─world_timestamps.csv
Now we can load and use the exported data with the same PyNeon API:
[10]:
# Load the exported cloud format data
rec_cloud = Recording(export_dir)
gaze_cloud = rec_cloud.gaze
# Verify that the data is identical
print("Exported gaze data (first 5 rows):")
print(gaze_cloud.data.head())
print("\nData shapes match:", gaze.data.shape == gaze_cloud.data.shape)
Exported gaze data (first 5 rows):
gaze x [px] gaze y [px] worn azimuth [deg] \
timestamp [ns]
1766074431275967547 731.885864 503.253845 -1 -4.384848
1766074431280967547 735.500916 502.152618 -1 -4.152129
1766074431285967547 735.843140 499.517426 -1 -4.130098
1766074431290967547 735.056641 502.690063 -1 -4.180729
1766074431295967547 736.322205 501.840668 -1 -4.099258
elevation [deg]
timestamp [ns]
1766074431275967547 6.207878
1766074431280967547 6.278540
1766074431285967547 6.447632
1766074431290967547 6.244054
1766074431295967547 6.298557
Data shapes match: True