> ## 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.

# Scene SDK Workflows

The Scene SDK (`encord.beta.scene`) is the recommended way to register point clouds and synchronized sensor streams in Encord. It lets you build Scenes from point cloud, image, camera, and frame-of-reference streams without writing raw Scene JSON.

Use this guide for the workflow. Use the [Scene SDK reference](/sdk-documentation/sdk-references/beta.scene.builder) for method-level details.

## Build and Upload a Scene

Build the Scene with `SceneBuilder`, then register it into **Files & Folders** with `StorageFolder.upload_scene`.

```python Build and Upload a Scene expandable theme={"dark"}
from uuid import UUID

from encord.beta.scene import SceneBuilder, identity_pose, intrinsics_pinhole
from encord.storage import StorageFolder


def build_scene() -> SceneBuilder:
    scene = SceneBuilder()

    # Anchor the sensor streams to the moving platform.
    ego = scene.add_for_stream("ego")
    ego.add_pose(identity_pose(), timestamp=0)

    # Matching timestamps make the point cloud and image appear together.
    scene.add_pcd_stream("lidar", frame_of_reference=ego).add_pcd(
        uri="s3://my-bucket/scenes/scene-001/lidar/000000.pcd",
        timestamp=0,
    )

    # Inline camera parameters create a camera stream named "front/camera".
    scene.add_image_stream(
        "front",
        width=1920,
        height=1080,
        intrinsics=intrinsics_pinhole(fx=1000, fy=1000, ox=960, oy=540),
        timestamp=0,
        frame_of_reference=ego,
    ).add_image(
        uri="s3://my-bucket/scenes/scene-001/front/000000.jpg",
        timestamp=0,
    )

    return scene


def register_scene(storage_folder: StorageFolder, integration_id: str) -> UUID:
    scene = build_scene()

    # Pass the SceneBuilder itself. upload_scene validates and serializes it internally.
    return storage_folder.upload_scene(
        scene=scene,
        title="scene-001",
        integration_id=integration_id,
        client_metadata={"source": "sample"},
    )
```

`upload_scene` returns the new Scene item UUID. Add that item to a Dataset before creating an annotation Project.

## Add Multi-frame Sensor Streams

Create stream builders once, then append events. Use the same timestamp for events that should appear together in the editor.

```python Multi-frame Scene Streams expandable theme={"dark"}

from encord.beta.scene import Direction, SceneBuilder, intrinsics_pinhole, translation_only


def build_multiframe_scene(frame_count: int = 3) -> SceneBuilder:
    scene = SceneBuilder()
    scene.set_world_convention(x=Direction.FORWARD, y=Direction.LEFT, z=Direction.UP)
    scene.set_camera_convention(x=Direction.RIGHT, y=Direction.DOWN, z=Direction.FORWARD)

    ego = scene.add_for_stream("ego")
    lidar = scene.add_pcd_stream(
        "lidar",
        frame_of_reference=ego,
        pose=translation_only(x=0.0, y=0.0, z=1.8),
    )
    front = scene.add_image_stream(
        "front",
        width=1920,
        height=1080,
        intrinsics=intrinsics_pinhole(fx=1000, fy=1000, ox=960, oy=540),
        timestamp=0,
        frame_of_reference=ego,
        pose=translation_only(x=1.2, y=0.0, z=1.5),
    )

    for frame in range(frame_count):
        ego.add_pose(translation_only(x=frame * 0.5, y=0.0, z=0.0), timestamp=frame)
        lidar.add_pcd(
            uri=f"s3://my-bucket/scenes/scene-001/lidar/{frame:06d}.pcd",
            timestamp=frame,
        )
        front.add_image(
            uri=f"s3://my-bucket/scenes/scene-001/front/{frame:06d}.jpg",
            timestamp=frame,
        )

    return scene
```

Use integer timestamps for frame-indexed data. Use sensor timestamps when streams are sampled at different rates.

## Coordinate Conventions

Set coordinate conventions in the SDK when your data uses known world and camera axes. For example, driving datasets often use a world convention such as forward, left, up and camera axes such as right, down, forward.

Use `SceneBuilder.set_world_convention` and `SceneBuilder.set_camera_convention` with `Direction` values. The world and camera conventions must have matching handedness.

## Read and Copy Registered Scenes

`SceneReader` fetches a registered Scene and returns signed URLs for its constituent files. It can also convert a registered Scene back into a `DataUploadScene` for migration or copying.

```python Read and Copy a Scene expandable theme={"dark"}

from uuid import UUID

from encord.beta.scene import SceneReader
from encord.orm.storage import DataUploadItems, StorageItemType
from encord.storage import StorageFolder, StorageItem


def find_scene(storage_folder: StorageFolder, title: str) -> StorageItem:
    scene_items = storage_folder.list_items(
        search=title,
        item_types=[StorageItemType.SCENE],
        page_size=1,
    )
    scene_item = next(iter(scene_items), None)
    if scene_item is None:
        raise ValueError(f"Scene with title '{title}' not found.")
    return scene_item


def get_lidar_signed_url(scene_item: StorageItem, timestamp: float = 0) -> str:
    scene = SceneReader(scene_item).read()
    lidar = scene.get_stream("lidar", kind="point_cloud")
    return lidar.get_event(timestamp=timestamp).signed_url


def get_image_signed_urls(scene_item: StorageItem, timestamp: float = 0) -> list[tuple[str, str]]:
    scene = SceneReader(scene_item).read()
    return [(stream_id, event.signed_url) for stream_id, event in scene.get_images_at_timestamp(timestamp)]


def copy_scene_to_folder(
    scene_item: StorageItem,
    target_storage_folder: StorageFolder,
    *,
    integration_id: str,
    title: str,
    source_uri_prefix: str,
    target_uri_prefix: str,
) -> UUID:
    payload = SceneReader(scene_item).to_upload_payload(
        title=title,
        uri_mapper=lambda uri: uri.replace(source_uri_prefix, target_uri_prefix),
    )

    return target_storage_folder.add_private_data_to_folder_start(
        integration_id=integration_id,
        private_files=DataUploadItems(scenes=[payload]),
    )
```

Use `find_stream` and `find_event` when a missing stream or timestamp is acceptable. Use `get_stream` and `get_event` when missing data should fail fast.

## Camera Distortion Models

The supported distortion models are defined in [`encord.beta.scene.intrinsics`](/sdk-documentation/sdk-references/beta.scene.intrinsics).

| SDK API                                    | Model type            | Distortion coefficients                        | Use when                                                                   |
| ------------------------------------------ | --------------------- | ---------------------------------------------- | -------------------------------------------------------------------------- |
| `intrinsics_pinhole(...)`                  | `pinhole`             | None                                           | Use for calibrated pinhole cameras with no distortion.                     |
| `intrinsics_radial(...)`                   | `radial`              | `k1`, `k2`, `k3`                               | Use when calibration only supplies radial coefficients.                    |
| `intrinsics_plumb_bob(...)`                | `plumb_bob`           | `k1`, `k2`, `k3`, `t1`, `t2`                   | Use for Brown-Conrady calibration with radial and tangential coefficients. |
| `intrinsics_fisheye(...)`                  | `fisheye`             | `k1`, `k2`, `k3`, `k4`                         | Use for fisheye lenses calibrated with four coefficients.                  |
| `intrinsics_rational_polynomial(...)`      | `rational_polynomial` | `k1`, `k2`, `k3`, `k4`, `k5`, `k6`, `t1`, `t2` | Use for OpenCV rational-polynomial calibration.                            |
| `intrinsics_simple(..., model="division")` | `division`            | `k`                                            | Use for one-parameter division-model calibration.                          |
| `intrinsics_simple(..., model="ucm")`      | `ucm`                 | `xi`, `k1`, `k2`, `k3`                         | Use for unified camera model calibration.                                  |
| `intrinsics_cylindrical(...)`              | `cylindrical`         | None                                           | Use for cylindrical camera projections.                                    |

```python Camera Distortion Models expandable theme={"dark"}

from encord.beta.scene import (
    SimpleIntrinsics,
    intrinsics_cylindrical,
    intrinsics_fisheye,
    intrinsics_pinhole,
    intrinsics_plumb_bob,
    intrinsics_radial,
    intrinsics_rational_polynomial,
    intrinsics_simple,
)


def build_distortion_model_examples() -> dict[str, SimpleIntrinsics]:
    return {
        "pinhole": intrinsics_pinhole(fx=1000, fy=1000, ox=960, oy=540),
        "radial": intrinsics_radial(fx=1000, fy=1000, ox=960, oy=540, k1=0.1, k2=-0.01, k3=0.001),
        "plumb_bob": intrinsics_plumb_bob(
            fx=1000,
            fy=1000,
            ox=960,
            oy=540,
            k1=0.1,
            k2=-0.01,
            k3=0.001,
            t1=0.0001,
            t2=-0.0001,
        ),
        "fisheye": intrinsics_fisheye(
            fx=1000,
            fy=1000,
            ox=960,
            oy=540,
            k1=0.1,
            k2=-0.01,
            k3=0.001,
            k4=-0.0001,
        ),
        "rational_polynomial": intrinsics_rational_polynomial(
            fx=1000,
            fy=1000,
            ox=960,
            oy=540,
            k1=0.1,
            k2=-0.01,
            k3=0.001,
            k4=-0.0001,
            k5=0.00001,
            k6=-0.000001,
            t1=0.0001,
            t2=-0.0001,
        ),
        "division": intrinsics_simple(fx=1000, fy=1000, ox=960, oy=540, model="division", k=0.01),
        "ucm": intrinsics_simple(fx=1000, fy=1000, ox=960, oy=540, model="ucm", xi=0.5, k1=0.1, k2=0, k3=0),
        "cylindrical": intrinsics_cylindrical(fx=1000, fy=1000, ox=960, oy=540),
    }
```

For most workflows, use `intrinsics_pinhole` or a dedicated distortion constructor. Use `intrinsics_advanced` only when you already have full calibration matrices and need to supply them directly.

## Validation Checklist

Before handing Scenes to annotators:

* Confirm every point cloud and image URI is accessible by the integration used for registration.
* Keep stream names stable and descriptive, such as `lidar`, `front`, `rear`, or `ego`.
* Use integer timestamps for frame-indexed data unless you need sensor-time alignment.
* Open at least one registered Scene in the editor to verify orientation, calibration, and frame navigation.

<Card title="Create and Register Scenes" icon="cube" href="/platform-documentation/Curate/add-files/add-pcd-data">
  Learn the Scene data model and registration choices.
</Card>
