> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encord.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCAP Scene labels

Unlike a video or an image sequence, an MCAP Scene has no frame grid. It stores 3D labels as events on a nanosecond timeline. Point cloud segmentation and time-series annotations use label spaces.

An uploaded MCAP recording is a **continuous MCAP Scene**. In the SDK, its label row is event-based: `LabelRowV2.is_event_based` is `True`.

<Warning>
  Do not use the frame-based `ObjectInstance` methods, such as `set_for_frames()` and `remove_from_frames()`, on a continuous MCAP Scene. Use `upsert_event()`, `delete_event()`, and `get_events()` for 3D objects, and `get_space()` with `put_object_instance()` for point cloud segmentation and time-series ranges.
</Warning>

## Writing Labels

Each example below is a standalone script with two tabs: **Single Scene** labels one MCAP Scene, and **Bulk** labels several Scenes, using bundles to initialize and save the label rows. Replace the values in the `# User input` section, the labels dictionary in the Bulk scripts, and the coordinates, with your own values. The ontology must contain the corresponding shape: a cuboid, point cloud segmentation, or time range. Each script saves the labels when run.

### 3D Objects

Root 3D objects, such as cuboids, spheres, keypoints, and polylines, use events at scene-relative nanosecond timestamps. Each upsert sets the geometry until the next event; a delete ends the object at that timestamp. The SDK uses hold behavior rather than interpolating geometry between keyframes. The example below labels a cuboid over `[2 s, 5 s)`, updating it at 3.5 s.

<Info>
  Encord's 3D annotation event model follows Foxglove's add/replace/delete model. See [SceneUpdate](https://docs.foxglove.dev/docs/sdk/schemas/scene-update) for updates and deletions, and [SceneEntity](https://docs.foxglove.dev/docs/sdk/schemas/scene-entity) for object IDs, geometry, and lifetime.
</Info>

<CodeGroup>
  ```python MCAP Cuboids - Single Scene expandable theme={"dark"}

  # Import dependencies
  import json

  from encord import EncordUserClient, Project
  from encord.objects import LabelRowV2, Object, ObjectInstance
  from encord.objects.coordinates import CuboidCoordinates

  # User input
  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique ID for the Project
  DATA_TITLE = "spot-walk-001.mcap"  # Replace with the title of the MCAP Scene
  OBJECT_TITLE = "Vehicle"  # Replace with the title of a cuboid object in the Ontology
  START_NS = 2_000_000_000  # Nanoseconds after the Scene start when the cuboid appears
  UPDATE_NS = 3_500_000_000  # Nanoseconds after the Scene start when the cuboid geometry changes
  END_NS = 5_000_000_000  # Nanoseconds after the Scene start when the cuboid ends
  OUTPUT_JSON = "/Users/chris-encord/mcap-cuboids-labels.json"  # Replace with the file path to save the exported labels

  # Create user client using ssh key
  user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      # For US platform users use "https://api.us.encord.com"
      domain="https://api.encord.com",
  )

  # Get the Project
  project: Project = user_client.get_project(PROJECT_ID)

  # Get and initialize the label row for the MCAP Scene
  label_rows = project.list_label_rows_v2(data_title_eq=DATA_TITLE)
  if len(label_rows) != 1:
      raise ValueError(f"Expected one label row; found {len(label_rows)}")
  label_row: LabelRowV2 = label_rows[0]
  label_row.initialise_labels()
  if not label_row.is_event_based:
      raise ValueError("Expected a continuous MCAP Scene")

  # Find the cuboid object in the Project Ontology
  cuboid_ontology_object: Object = project.ontology_structure.get_child_by_title(title=OBJECT_TITLE, type_=Object)
  if cuboid_ontology_object is None:
      raise ValueError(f"No ontology object named {OBJECT_TITLE!r}")

  # Add a cuboid over [START_NS, END_NS), with a geometry change at UPDATE_NS.
  # Create a cuboid instance and add it to the label row
  cuboid_object_instance: ObjectInstance = cuboid_ontology_object.create_instance()
  label_row.add_object_instance(cuboid_object_instance)

  # Set the cuboid geometry from START_NS
  cuboid_object_instance.upsert_event(
      CuboidCoordinates(
          position=(1.0, 2.0, 0.8),
          orientation=(0.0, 0.0, 0.25),
          size=(4.2, 1.8, 1.6),
      ),
      START_NS,
  )

  # Change the cuboid geometry from UPDATE_NS
  cuboid_object_instance.upsert_event(
      CuboidCoordinates(
          position=(2.2, 2.1, 0.8),
          orientation=(0.0, 0.0, 0.30),
          size=(4.2, 1.8, 1.6),
      ),
      UPDATE_NS,
  )

  # End the cuboid at END_NS
  cuboid_object_instance.delete_event(END_NS)

  # Print the cuboid events
  for event in cuboid_object_instance.get_events():
      if event.kind == "upsert":
          print(event.frame, event.kind, event.coordinates)
      else:
          print(event.frame, event.kind)

  # Export the in-memory labels to a JSON file
  with open(OUTPUT_JSON, "w", encoding="utf-8") as file:
      json.dump(label_row.to_encord_dict(), file, ensure_ascii=False, indent=2)

  # Save the labels to Encord
  label_row.save()
  print(f"Saved label row for {label_row.data_title}")
  ```

  ```python MCAP Cuboids - Bulk expandable theme={"dark"}

  # Import dependencies
  import json

  from encord import EncordUserClient, Project
  from encord.objects import LabelRowV2, Object, ObjectInstance
  from encord.objects.coordinates import CuboidCoordinates

  # User input
  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique ID for the Project
  OBJECT_TITLE = "Vehicle"  # Replace with the title of a cuboid object in the Ontology
  BUNDLE_SIZE = 100
  OUTPUT_JSON = "/Users/chris-encord/mcap-cuboids-labels-bulk.json"  # Replace with the file path to save the exported labels

  # Dictionary of cuboid events per MCAP Scene and per cuboid.
  # "frame" is in nanoseconds after the Scene start.
  # An "upsert" sets the cuboid geometry until the next event; a "delete" ends the cuboid.
  mcap_cuboid_labels = {
      "spot-walk-001.mcap": {
          "vehicle_001": [
              {
                  "frame": 2_000_000_000,
                  "kind": "upsert",
                  "coordinates": CuboidCoordinates(
                      position=(1.0, 2.0, 0.8),
                      orientation=(0.0, 0.0, 0.25),
                      size=(4.2, 1.8, 1.6),
                  ),
              },
              {
                  "frame": 3_500_000_000,
                  "kind": "upsert",
                  "coordinates": CuboidCoordinates(
                      position=(2.2, 2.1, 0.8),
                      orientation=(0.0, 0.0, 0.30),
                      size=(4.2, 1.8, 1.6),
                  ),
              },
              {"frame": 5_000_000_000, "kind": "delete"},
          ],
          "vehicle_002": [
              {
                  "frame": 1_000_000_000,
                  "kind": "upsert",
                  "coordinates": CuboidCoordinates(
                      position=(-3.0, 1.5, 0.8),
                      orientation=(0.0, 0.0, 0.10),
                      size=(4.5, 1.9, 1.7),
                  ),
              },
              {"frame": 4_000_000_000, "kind": "delete"},
          ],
      },
      "spot-walk-002.mcap": {
          "vehicle_003": [
              {
                  "frame": 500_000_000,
                  "kind": "upsert",
                  "coordinates": CuboidCoordinates(
                      position=(5.0, -1.0, 0.8),
                      orientation=(0.0, 0.0, 1.57),
                      size=(4.0, 1.8, 1.5),
                  ),
              },
              {"frame": 2_500_000_000, "kind": "delete"},
          ],
      },
  }

  # Create user client using ssh key
  user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      # For US platform users use "https://api.us.encord.com"
      domain="https://api.encord.com",
  )

  # Get the Project
  project: Project = user_client.get_project(PROJECT_ID)

  # Find the cuboid object in the Project Ontology
  cuboid_ontology_object: Object = project.ontology_structure.get_child_by_title(title=OBJECT_TITLE, type_=Object)
  if cuboid_ontology_object is None:
      raise ValueError(f"No ontology object named {OBJECT_TITLE!r}")

  # Initialize the label rows for all MCAP Scenes using a bundle
  label_row_map = {}
  with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
      for data_title in mcap_cuboid_labels.keys():
          label_rows = project.list_label_rows_v2(data_title_eq=data_title)
          if not label_rows:
              print(f"Skipping: No label row found for {data_title}")
              continue
          label_row: LabelRowV2 = label_rows[0]
          label_row.initialise_labels(bundle=bundle)
          label_row_map[data_title] = label_row

  # Add a cuboid over each upsert/delete sequence, per MCAP Scene
  label_rows_to_save = []
  for data_title, cuboids in mcap_cuboid_labels.items():
      label_row = label_row_map.get(data_title)
      if not label_row:
          continue
      if not label_row.is_event_based:
          print(f"Skipping: {data_title} is not a continuous MCAP Scene")
          continue

      for label_ref, events in cuboids.items():
          # Create a cuboid instance and add it to the label row
          cuboid_object_instance: ObjectInstance = cuboid_ontology_object.create_instance()
          label_row.add_object_instance(cuboid_object_instance)

          # Write the cuboid events in order
          for event in events:
              if event["kind"] == "upsert":
                  cuboid_object_instance.upsert_event(event["coordinates"], event["frame"])
              else:
                  cuboid_object_instance.delete_event(event["frame"])

      label_rows_to_save.append(label_row)

  # Export the in-memory labels for all label rows to a JSON file
  with open(OUTPUT_JSON, "w", encoding="utf-8") as file:
      json.dump([label_row.to_encord_dict() for label_row in label_rows_to_save], file, ensure_ascii=False, indent=2)

  # Save all label rows to Encord using a bundle
  with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
      for label_row in label_rows_to_save:
          label_row.save(bundle=bundle)
          print(f"Saved label row for {label_row.data_title}")
  ```
</CodeGroup>

**Useful Concepts**:

**Interpolation uses hold behavior.** A 3D object retains the geometry from its latest upsert until the next event. In the cuboid example, the geometry at 3 s is the geometry written at 2 s; at 4 s it is the geometry written at 3.5 s. Positions and rotations are not linearly interpolated between label events. Sensor frame transforms have their own [interpolation behavior](/platform-documentation/Curate/add-files/mcap#frames-of-reference-and-synchronization).

**Stored events and resolved annotations are different.** `object_instance.get_events()` returns stored upserts and deletes in timestamp order; only upserts have coordinates. `object_instance.get_ranges()` returns the inclusive ranges where the object exists. To read its geometry between events, use `object_instance.get_annotation(frame=3_000_000_000).coordinates`. Despite the parameter name `frame`, this is a nanosecond timestamp relative to the Scene start. The resolved annotation is read-only; use `upsert_event()` to write a change.

**A delete ends the object immediately.** An upsert at 2 s followed by a delete at 5 s means the object exists over `[2 s, 5 s)`: it is absent at exactly 5 s, and reading it there raises `LabelRowError`. The corresponding inclusive SDK range ends at `4_999_999_999` ns. Every track must end with a delete before export or save; if the last included timestamp is `t`, delete at `t + 1` ns. Point-index ranges and time-series ranges include both endpoints.

### Point Cloud Segmentation

Address a point cloud message with its topic and **absolute MCAP `log_time` in nanoseconds**. Point index ranges are inclusive and refer to the exact point order in that message. Filtering or reordering the point cloud changes those indices.

To read the `log_time` with the Go version of the [MCAP CLI](https://mcap.dev/guides/cli), extract the first column of its text output (integer nanoseconds). Replace the file and topic with your recording's values:

```bash theme={"dark"}
mcap cat recording.mcap --topics /lidar/smooth_pointcloud | head -n 2 | awk '{print $1}'
```

<CodeGroup>
  ```python MCAP Point Cloud Segmentation - Single Scene expandable theme={"dark"}

  # Import dependencies
  import json

  from encord import EncordUserClient, Project
  from encord.objects import LabelRowV2, Object, ObjectInstance
  from encord.objects.frames import Range
  from encord.objects.spaces.range_space.point_cloud_space import PointCloudFileSpace

  # User input
  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique ID for the Project
  DATA_TITLE = "spot-walk-001.mcap"  # Replace with the title of the MCAP Scene
  OBJECT_TITLE = "Road surface"  # Replace with the title of a point cloud segmentation object in the Ontology
  POINT_CLOUD_TOPIC = "/lidar/smooth_pointcloud"  # Replace with the point cloud topic in the MCAP file
  LOG_TIME_NS = 1_750_258_475_125_212_159  # Replace with the absolute MCAP log_time of the message, in nanoseconds
  POINT_RANGES = [
      Range(start=0, end=15_234),
      Range(start=52_000, end=52_980),
  ]  # Inclusive point index ranges in the message
  OUTPUT_JSON = "/Users/chris-encord/mcap-segmentation-labels.json"  # Replace with the file path to save the exported labels

  # Create user client using ssh key
  user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      # For US platform users use "https://api.us.encord.com"
      domain="https://api.encord.com",
  )

  # Get the Project
  project: Project = user_client.get_project(PROJECT_ID)

  # Get and initialize the label row for the MCAP Scene
  label_rows = project.list_label_rows_v2(data_title_eq=DATA_TITLE)
  if len(label_rows) != 1:
      raise ValueError(f"Expected one label row; found {len(label_rows)}")
  label_row: LabelRowV2 = label_rows[0]
  label_row.initialise_labels()
  if not label_row.is_event_based:
      raise ValueError("Expected a continuous MCAP Scene")

  # Find the segmentation object in the Project Ontology
  segmentation_ontology_object: Object = project.ontology_structure.get_child_by_title(
      title=OBJECT_TITLE, type_=Object
  )
  if segmentation_ontology_object is None:
      raise ValueError(f"No ontology object named {OBJECT_TITLE!r}")

  # Label inclusive point-index ranges on one point cloud message.
  # Get the point cloud space for one message, using the <topic>@<log_time_ns> space ID
  point_cloud_space: PointCloudFileSpace = label_row.get_space(
      id=f"{POINT_CLOUD_TOPIC}@{LOG_TIME_NS}", type_="point_cloud"
  )

  # Create a segmentation instance and label the point index ranges
  segmentation_object_instance: ObjectInstance = segmentation_ontology_object.create_instance()
  point_cloud_space.put_object_instance(segmentation_object_instance, ranges=POINT_RANGES)

  # Export the in-memory labels to a JSON file
  with open(OUTPUT_JSON, "w", encoding="utf-8") as file:
      json.dump(label_row.to_encord_dict(), file, ensure_ascii=False, indent=2)

  # Save the labels to Encord
  label_row.save()
  print(f"Saved label row for {label_row.data_title}")
  ```

  ```python MCAP Point Cloud Segmentation - Bulk expandable theme={"dark"}

  # Import dependencies
  import json

  from encord import EncordUserClient, Project
  from encord.objects import LabelRowV2, Object, ObjectInstance
  from encord.objects.frames import Range
  from encord.objects.spaces.range_space.point_cloud_space import PointCloudFileSpace

  # User input
  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique ID for the Project
  OBJECT_TITLE = "Road surface"  # Replace with the title of a point cloud segmentation object in the Ontology
  BUNDLE_SIZE = 100
  OUTPUT_JSON = "/Users/chris-encord/mcap-segmentation-labels-bulk.json"  # Replace with the file path to save the exported labels

  # Dictionary of segmentations per MCAP Scene.
  # "log_time_ns" is the absolute MCAP log_time of the point cloud message, in nanoseconds.
  # "ranges" are inclusive point index ranges in that message.
  mcap_segmentation_labels = {
      "spot-walk-001.mcap": [
          {
              "topic": "/lidar/smooth_pointcloud",
              "log_time_ns": 1_750_258_475_125_212_159,
              "ranges": [Range(start=0, end=15_234), Range(start=52_000, end=52_980)],
          },
          {
              "topic": "/lidar/smooth_pointcloud",
              "log_time_ns": 1_750_258_475_225_198_004,
              "ranges": [Range(start=0, end=14_987)],
          },
      ],
      "spot-walk-002.mcap": [
          {
              "topic": "/lidar/smooth_pointcloud",
              "log_time_ns": 1_750_259_102_004_117_392,
              "ranges": [Range(start=1_200, end=18_450)],
          },
      ],
  }

  # Create user client using ssh key
  user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      # For US platform users use "https://api.us.encord.com"
      domain="https://api.encord.com",
  )

  # Get the Project
  project: Project = user_client.get_project(PROJECT_ID)

  # Find the segmentation object in the Project Ontology
  segmentation_ontology_object: Object = project.ontology_structure.get_child_by_title(
      title=OBJECT_TITLE, type_=Object
  )
  if segmentation_ontology_object is None:
      raise ValueError(f"No ontology object named {OBJECT_TITLE!r}")

  # Initialize the label rows for all MCAP Scenes using a bundle
  label_row_map = {}
  with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
      for data_title in mcap_segmentation_labels.keys():
          label_rows = project.list_label_rows_v2(data_title_eq=data_title)
          if not label_rows:
              print(f"Skipping: No label row found for {data_title}")
              continue
          label_row: LabelRowV2 = label_rows[0]
          label_row.initialise_labels(bundle=bundle)
          label_row_map[data_title] = label_row

  # Label inclusive point-index ranges on each point cloud message, per MCAP Scene
  label_rows_to_save = []
  for data_title, segmentations in mcap_segmentation_labels.items():
      label_row = label_row_map.get(data_title)
      if not label_row:
          continue
      if not label_row.is_event_based:
          print(f"Skipping: {data_title} is not a continuous MCAP Scene")
          continue

      for item in segmentations:
          # Get the point cloud space for one message, using the <topic>@<log_time_ns> space ID
          point_cloud_space: PointCloudFileSpace = label_row.get_space(
              id=f"{item['topic']}@{item['log_time_ns']}", type_="point_cloud"
          )

          # Create a segmentation instance and label the point index ranges
          segmentation_object_instance: ObjectInstance = segmentation_ontology_object.create_instance()
          point_cloud_space.put_object_instance(segmentation_object_instance, ranges=item["ranges"])

      label_rows_to_save.append(label_row)

  # Export the in-memory labels for all label rows to a JSON file
  with open(OUTPUT_JSON, "w", encoding="utf-8") as file:
      json.dump([label_row.to_encord_dict() for label_row in label_rows_to_save], file, ensure_ascii=False, indent=2)

  # Save all label rows to Encord using a bundle
  with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
      for label_row in label_rows_to_save:
          label_row.save(bundle=bundle)
          print(f"Saved label row for {label_row.data_title}")
  ```
</CodeGroup>

### Time Series

Use the exact numeric sub-channel ID, such as `/spot/cmd_vel.angular.x`, rather than only the parent topic. Both endpoints of a time-series `Range` are inclusive and expressed in nanoseconds relative to the Scene start. See [Time Series layouts](/platform-documentation/Curate/add-files/mcap#define-time-series-channels) to identify a sub-channel in your recording.

<CodeGroup>
  ```python MCAP Time Series - Single Scene expandable theme={"dark"}

  # Import dependencies
  import json

  from encord import EncordUserClient, Project
  from encord.objects import LabelRowV2, Object, ObjectInstance
  from encord.objects.frames import Range
  from encord.objects.spaces.range_space.time_series_space import TimeSeriesSpace

  # User input
  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique ID for the Project
  DATA_TITLE = "spot-walk-001.mcap"  # Replace with the title of the MCAP Scene
  OBJECT_TITLE = "Braking interval"  # Replace with the title of a time range object in the Ontology
  CHANNEL_ID = "/spot/cmd_vel.angular.x"  # Replace with the numeric sub-channel ID in the MCAP file
  START_NS = 2_250_000_000  # Nanoseconds after the Scene start when the range starts (inclusive)
  END_NS = 2_900_000_000  # Nanoseconds after the Scene start when the range ends (inclusive)
  OUTPUT_JSON = "/Users/chris-encord/mcap-time-series-labels.json"  # Replace with the file path to save the exported labels

  # Create user client using ssh key
  user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      # For US platform users use "https://api.us.encord.com"
      domain="https://api.encord.com",
  )

  # Get the Project
  project: Project = user_client.get_project(PROJECT_ID)

  # Get and initialize the label row for the MCAP Scene
  label_rows = project.list_label_rows_v2(data_title_eq=DATA_TITLE)
  if len(label_rows) != 1:
      raise ValueError(f"Expected one label row; found {len(label_rows)}")
  label_row: LabelRowV2 = label_rows[0]
  label_row.initialise_labels()
  if not label_row.is_event_based:
      raise ValueError("Expected a continuous MCAP Scene")

  # Find the time range object in the Project Ontology
  time_range_ontology_object: Object = project.ontology_structure.get_child_by_title(title=OBJECT_TITLE, type_=Object)
  if time_range_ontology_object is None:
      raise ValueError(f"No ontology object named {OBJECT_TITLE!r}")

  # Add an inclusive range to one numeric MCAP channel.
  # Get the time series space for the channel
  time_series_space: TimeSeriesSpace = label_row.get_space(id=CHANNEL_ID, type_="time_series")

  # Create a time range instance and add it to the channel
  time_range_object_instance: ObjectInstance = time_range_ontology_object.create_instance()
  time_series_space.put_object_instance(time_range_object_instance, ranges=Range(start=START_NS, end=END_NS))

  # Export the in-memory labels to a JSON file
  with open(OUTPUT_JSON, "w", encoding="utf-8") as file:
      json.dump(label_row.to_encord_dict(), file, ensure_ascii=False, indent=2)

  # Save the labels to Encord
  label_row.save()
  print(f"Saved label row for {label_row.data_title}")
  ```

  ```python MCAP Time Series - Bulk expandable theme={"dark"}

  # Import dependencies
  import json

  from encord import EncordUserClient, Project
  from encord.objects import LabelRowV2, Object, ObjectInstance
  from encord.objects.frames import Range
  from encord.objects.spaces.range_space.time_series_space import TimeSeriesSpace

  # User input
  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique ID for the Project
  OBJECT_TITLE = "Braking interval"  # Replace with the title of a time range object in the Ontology
  BUNDLE_SIZE = 100
  OUTPUT_JSON = "/Users/chris-encord/mcap-time-series-labels-bulk.json"  # Replace with the file path to save the exported labels

  # Dictionary of time ranges per MCAP Scene.
  # "channel_id" is the numeric sub-channel ID in the MCAP file.
  # "start_ns" and "end_ns" are inclusive, in nanoseconds after the Scene start.
  mcap_time_series_labels = {
      "spot-walk-001.mcap": [
          {"channel_id": "/spot/cmd_vel.angular.x", "start_ns": 2_250_000_000, "end_ns": 2_900_000_000},
          {"channel_id": "/spot/cmd_vel.linear.x", "start_ns": 6_000_000_000, "end_ns": 7_250_000_000},
      ],
      "spot-walk-002.mcap": [
          {"channel_id": "/spot/cmd_vel.angular.x", "start_ns": 1_000_000_000, "end_ns": 1_800_000_000},
      ],
  }

  # Create user client using ssh key
  user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      # For US platform users use "https://api.us.encord.com"
      domain="https://api.encord.com",
  )

  # Get the Project
  project: Project = user_client.get_project(PROJECT_ID)

  # Find the time range object in the Project Ontology
  time_range_ontology_object: Object = project.ontology_structure.get_child_by_title(title=OBJECT_TITLE, type_=Object)
  if time_range_ontology_object is None:
      raise ValueError(f"No ontology object named {OBJECT_TITLE!r}")

  # Initialize the label rows for all MCAP Scenes using a bundle
  label_row_map = {}
  with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
      for data_title in mcap_time_series_labels.keys():
          label_rows = project.list_label_rows_v2(data_title_eq=data_title)
          if not label_rows:
              print(f"Skipping: No label row found for {data_title}")
              continue
          label_row: LabelRowV2 = label_rows[0]
          label_row.initialise_labels(bundle=bundle)
          label_row_map[data_title] = label_row

  # Add an inclusive range to each numeric MCAP channel, per MCAP Scene
  label_rows_to_save = []
  for data_title, time_ranges in mcap_time_series_labels.items():
      label_row = label_row_map.get(data_title)
      if not label_row:
          continue
      if not label_row.is_event_based:
          print(f"Skipping: {data_title} is not a continuous MCAP Scene")
          continue

      for item in time_ranges:
          # Get the time series space for the channel
          time_series_space: TimeSeriesSpace = label_row.get_space(id=item["channel_id"], type_="time_series")

          # Create a time range instance and add it to the channel
          time_range_object_instance: ObjectInstance = time_range_ontology_object.create_instance()
          time_series_space.put_object_instance(
              time_range_object_instance, ranges=Range(start=item["start_ns"], end=item["end_ns"])
          )

      label_rows_to_save.append(label_row)

  # Export the in-memory labels for all label rows to a JSON file
  with open(OUTPUT_JSON, "w", encoding="utf-8") as file:
      json.dump([label_row.to_encord_dict() for label_row in label_rows_to_save], file, ensure_ascii=False, indent=2)

  # Save all label rows to Encord using a bundle
  with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
      for label_row in label_rows_to_save:
          label_row.save(bundle=bundle)
          print(f"Saved label row for {label_row.data_title}")
  ```
</CodeGroup>

## Reading Labels

### JSON export

Initialize the label row to read its saved annotations. This example exports all loaded labels to JSON.

```python export MCAP labels expandable theme={"dark"}
import json

from encord import EncordUserClient

client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path="file-path-to-ssh-key",
)
project = client.get_project("<project_id>")
rows = project.list_label_rows_v2(data_title_eq="<mcap_data_title>")
label_row = rows[0]
label_row.initialise_labels()

with open("mcap-labels.json", "w") as file:
    json.dump(label_row.to_encord_dict(), file, ensure_ascii=False, indent=2)
```

### Other SDK methods

For root 3D objects, `frame` means nanoseconds relative to the Scene start. `obj` below is an `ObjectInstance`:

| API                                                 | When to use it                                                                         |
| --------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `label_row.is_event_based`                          | Check whether the row uses continuous events.                                          |
| `label_row.get_object_instances(filter_frames=...)` | Find root objects present at a timestamp or overlapping a `Range`.                     |
| `obj.get_events()`                                  | Inspect stored upserts and deletes in time order; only upserts have coordinates.       |
| `obj.get_ranges()`                                  | Find inclusive object lifetimes, including gaps. Requires a closing delete.            |
| `obj.get_annotation(frame=t)`                       | Read geometry at any timestamp using hold behavior; raises `LabelRowError` if absent.  |
| `annotation.keyframe`, `annotation.is_virtual`      | Identify the source upsert and whether the resolved view inherits an earlier keyframe. |

```python Read MCAP labels expandable theme={"dark"}
from encord import EncordUserClient

client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path="file-path-to-ssh-key",
)
project = client.get_project("<project_id>")
rows = project.list_label_rows_v2(data_title_eq="<mcap_data_title>")
label_row = rows[0]
label_row.initialise_labels()

# Root 3D objects
for object_instance in label_row.get_object_instances():
    print(object_instance.object_hash)
    for event in object_instance.get_events():
        if event.kind == "upsert":
            print(event.frame, event.kind, event.coordinates)
        else:
            print(event.frame, event.kind)

    for lifetime in object_instance.get_ranges():
        annotation = object_instance.get_annotation(frame=lifetime.start)
        print(lifetime.start, lifetime.end, annotation.coordinates)
```

For point cloud segmentation and time-series labels, use label spaces:

| API                                                            | When to use it                                                                   |
| -------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `label_row.get_space(id=..., type_=...)`                       | Access a point cloud message (`point_cloud`) or numeric channel (`time_series`). |
| `label_row.get_spaces()`                                       | List known label spaces; this does not list every MCAP topic or message.         |
| `space.get_object_instances()`, `space.get_object_ranges(obj)` | Read labeled objects and their inclusive point-index or time ranges.             |

For general 3D ontology examples and non-MCAP point cloud data, see [Point Cloud (3D)](/sdk-documentation/sdk-labels/sdk-labels-import-3d).
