Reading Sensor PointClouds#

Lidar and radar PointCloud RenderVars are composite render outputs. Mapping one output exposes one named tensor per requested channel plus CPU params that describe the output. Use Counts to bound per-point or per-detection tensors before reading channel values.

For the output container format, see Sensor Outputs. For sensor-specific channel meanings, see Lidar Sensors and Radar Sensors.

Python#

def read_lidar_pointcloud(frame) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """Map the PointCloud composite tensor to CPU and return valid point channels."""
    with frame.render_vars["PointCloud"].map(device=ovrtx.Device.CPU) as pointcloud:
        coordinates = np.from_dlpack(pointcloud["Coordinates"])
        counts = np.from_dlpack(pointcloud["Counts"])
        intensity = np.from_dlpack(pointcloud["Intensity"])
        time_offset_ns = np.from_dlpack(pointcloud["TimeOffsetNs"])

        # Counts contains the number of valid entries in the per-point tensors.
        valid_count = int(counts[0])
        points = np.asarray(coordinates[:, :valid_count].T)
        return (
            points.copy(),
            np.asarray(intensity[:valid_count]).copy(),
            np.asarray(time_offset_ns[:valid_count]).copy(),
        )


def read_radar_pointcloud(frame) -> tuple[np.ndarray, np.ndarray]:
    """Map the PointCloud composite tensor to CPU and return valid point channels."""
    with frame.render_vars["PointCloud"].map(device=ovrtx.Device.CPU) as pointcloud:
        coordinates = np.from_dlpack(pointcloud["Coordinates"])
        counts = np.from_dlpack(pointcloud["Counts"])
        radial_velocity = np.from_dlpack(pointcloud["RadialVelocityMs"])

        # Counts contains the number of valid detections in the composite tensors.
        valid_count = int(counts[0])
        points = np.asarray(coordinates[:, :valid_count].T)
        return (
            points.copy(),
            np.asarray(radial_velocity[:valid_count]).copy(),
        )


In Python, frame.render_vars["PointCloud"].map(device=ovrtx.Device.CPU) returns a mapped composite output. Index by exact channel name, then pass the channel object to a DLPack consumer such as NumPy. Copy arrays before leaving the with block when later code needs to keep the data.

C#

// Read one mapped lidar PointCloud. Counts is the valid point count; Intensity
// and TimeOffsetNs are per-point channels over that valid range.
bool print_pointcloud_summary(ovrtx_render_var_output_t const& output)
{
    // The USD also requests Coordinates; this example only needs scalar and
    // time channels for the printed statistics.
    int32_t const* counts_data = cpu_tensor_data<int32_t>(output, "Counts");
    float const* intensity_data = cpu_tensor_data<float>(output, "Intensity");
    int32_t const* time_offset_data =
        cpu_tensor_data<int32_t>(output, "TimeOffsetNs");

    if (!counts_data || !intensity_data || !time_offset_data) {
        return false;
    }

    // Counts is a scalar tensor containing the number of valid point entries.
    int32_t const valid_point_count = counts_data[0];
    if (valid_point_count <= 0) {
        std::cerr << "Expected at least one valid lidar point, got "
                  << valid_point_count << "\n";
        return false;
    }

    double intensity_sum = 0.0;
    int32_t max_time_offset_ns = time_offset_data[0];

    // Only the first Counts entries are valid in each per-point channel.
    for (int32_t i = 0; i < valid_point_count; ++i) {
        const float intensity = intensity_data[i];
        if (!std::isfinite(intensity)) {
            std::cerr << "Intensity tensor contained a non-finite value\n";
            return false;
        }
        intensity_sum += intensity;
        max_time_offset_ns = std::max(max_time_offset_ns, time_offset_data[i]);
    }

    const double mean_intensity =
        intensity_sum / static_cast<double>(valid_point_count);
    std::cout << "valid points=" << valid_point_count
              << ", mean intensity=" << mean_intensity
              << ", max point time offset to point cloud frame start=" << max_time_offset_ns << " ns\n";
    return true;
}
// Read the mapped PointCloud tensors for one step. Counts tells how many point
// entries are valid; RCS and RadialVelocityMs are per-detection values.
bool print_pointcloud_step(ovrtx_render_var_output_t const& output,
                           int step,
                           int* moving_detection_count,
                           float* max_abs_radial_velocity)
{
    // The USD requests these channels on the radar PointCloud RenderVar.
    int32_t const* counts_data = cpu_tensor_data<int32_t>(output, "Counts");
    float const* rcs_data = cpu_tensor_data<float>(output, "RCS");
    float const* radial_velocity_data =
        cpu_tensor_data<float>(output, "RadialVelocityMs");

    if (!counts_data || !rcs_data || !radial_velocity_data) {
        return false;
    }

    // Counts is a scalar tensor containing the number of valid point entries.
    int32_t const valid_point_count = counts_data[0];
    if (valid_point_count <= 0) {
        std::cerr << "Expected at least one valid radar point, got "
                  << valid_point_count << "\n";
        return false;
    }

    const size_t valid_points = static_cast<size_t>(valid_point_count);

    // RCS is the radar cross section channel returned for each detection.
    bool found_finite_rcs = false;
    float min_rcs = std::numeric_limits<float>::infinity();
    float max_rcs = -std::numeric_limits<float>::infinity();

    // RCS values are regular float channel entries in the composite output.
    for (size_t i = 0; i < valid_points; ++i) {
        const float rcs_value = rcs_data[i];
        if (std::isfinite(rcs_value)) {
            found_finite_rcs = true;
            min_rcs = std::min(min_rcs, rcs_value);
            max_rcs = std::max(max_rcs, rcs_value);
        }
    }
    if (!found_finite_rcs) {
        std::cerr << "RCS tensor was present but contained no finite values for valid points\n";
        return false;
    }

    bool found_finite_radial_velocity = false;
    float min_radial_velocity = std::numeric_limits<float>::infinity();
    float max_radial_velocity = -std::numeric_limits<float>::infinity();
    int step_moving_detection_count = 0;

    // Approaching objects report negative radial velocity; track magnitude when
    // reporting detections from the moving cube.
    for (size_t i = 0; i < valid_points; ++i) {
        const float radial_velocity = radial_velocity_data[i];
        if (!std::isfinite(radial_velocity)) {
            continue;
        }
        found_finite_radial_velocity = true;
        min_radial_velocity = std::min(min_radial_velocity, radial_velocity);
        max_radial_velocity = std::max(max_radial_velocity, radial_velocity);
        const float abs_radial_velocity = std::abs(radial_velocity);
        *max_abs_radial_velocity =
            std::max(*max_abs_radial_velocity, abs_radial_velocity);
        if (abs_radial_velocity > kMovingRadialVelocityThresholdMs) {
            ++step_moving_detection_count;
        }
    }
    if (!found_finite_radial_velocity) {
        std::cerr << "RadialVelocityMs tensor contained no finite values for valid points\n";
        return false;
    }

    *moving_detection_count += step_moving_detection_count;

    std::cout << "  step " << step
              << ": valid points=" << valid_point_count
              << ", RCS min/max=[" << min_rcs << ", " << max_rcs << "]"
              << ", radial velocity min/max=[" << min_radial_velocity << ", "
              << max_radial_velocity << "]"
              << " m/s\n";

    return true;
}

In C, map the PointCloud render var with ovrtx_map_render_var_output(), find named tensors in ovrtx_render_var_output_t::tensors, then unmap with ovrtx_unmap_render_var_output().

CPU and CUDA Mapping#

CPU mapping is easiest for examples, logging, and validation. CUDA mapping is available for GPU point-cloud pipelines:

  • Python uses map(device=ovrtx.Device.CUDA).

  • C uses OVRTX_MAP_DEVICE_TYPE_CUDA for linear CUDA memory.

GPU-mapped tensors are CUDA DLTensors. Consume them with GPU-aware code and respect the synchronization hints on the mapped output. CUDA_ARRAY mapping is intended for image-style outputs, not point-cloud channel tensors.

Rules#

  • Use Counts[0] before slicing or iterating per-point tensors.

  • Treat channel names as part of the data contract; they must match the channels authored on the PointCloud RenderVar.

  • Counts and Flags are auto-enabled by lidar and radar models.

  • Other payload channels are present only when requested.

  • Mapped tensor pointers are valid only until unmap.

  • Copy CPU data, or synchronize and copy GPU data, if it must outlive the mapping.

Note

The shapes shown below are the current non-tiled PointCloud layouts. Read tensor shapes from the mapped output descriptor at runtime instead of hard-coding allocation sizes.

Lidar Point Cloud#

Produced by the lidar sensor model.

ovrtx_render_var_output_t "PointCloud"
  name:    "PointCloud"

  tensors (all CUDA, Nmax is the per-frame allocation bound):
    "Coordinates"    -- [3, Nmax]   float32   (x, y, z per point; spherical or cartesian per coordsType)
    "Intensity"      -- [Nmax]      float32   (return intensity per point)
    "Flags"          -- [Nmax]      uint8     (validity / classification flags; auto-enabled)
    "Counts"         -- [1]         int32     (actual number of valid points this frame; auto-enabled)
    "TimeOffsetNs"   -- [Nmax]      int32     (per-point time offset from frame start)
    "EmitterId"      -- [Nmax]      uint32    (emitter / beam index)
    "ChannelId"      -- [Nmax]      uint32    (channel / detector index)
    "MaterialId"     -- [Nmax]      uint32    (material of hit surface)
    "TickId"         -- [Nmax]      uint32    (tick / scan index)
    "HitNormal"      -- [Nmax, 3]   float32   (surface normal at hit)
    "Velocity"       -- [Nmax, 3]   float32   (velocity at hit point)
    "ObjectId"       -- [Nmax, 4]   uint32    (128-bit instance ID, 4x uint32)
    "EchoId"         -- [Nmax]      uint8     (echo / return index)
    "TickState"      -- [Nmax]      uint8     (per-tick state)

  params (CPU):
    "frameId"                  -- uint64
    "timestampNs"              -- uint64
    "modality"                 -- uint32
    "coordsType"               -- uint32     (spherical | cartesian)
    "frameOfReference"         -- uint16     (sensor | parent | world | custom)
    "motionCompensationState"  -- uint16
    "modelToAppTransform"      -- float32 [4, 4]
    "frameStartTimeStampNs"    -- uint64
    "frameStartPosM"           -- float32 [3]
    "frameStartOrientation"    -- float32 [4]
    "frameEndTimeStampNs"      -- uint64
    "frameEndPosM"             -- float32 [3]
    "frameEndOrientation"      -- float32 [4]
    "maxPoints"                -- uint32     (maximum point allocation; use Counts for the valid per-frame count)

Field-by-field meaning, units, and visualization patterns are in Lidar Sensors.

Radar Point Cloud#

Produced by the radar sensor model.

ovrtx_render_var_output_t "PointCloud"
  name:    "PointCloud"

  tensors (all CUDA, Nmax is the per-frame allocation bound):
    "Coordinates"      -- [3, Nmax]  float32   (range, azimuth, elevation -- or x, y, z per coordsType)
    "RCS"              -- [Nmax]     float32   (radar cross section)
    "RadialVelocityMs" -- [Nmax]     float32   (radial velocity, m/s -- negative for approaching)
    "TimeOffsetNs"     -- [Nmax]     int32     (per-detection time offset from frame start)
    "Flags"            -- [Nmax]     uint8     (auto-enabled)
    "Counts"           -- [1]        int32     (actual number of valid detections this frame; auto-enabled)

  params (CPU):
    "frameId"                  -- uint64
    "timestampNs"              -- uint64
    "modality"                 -- uint32
    "coordsType"               -- uint32
    "frameOfReference"         -- uint16
    "modelToAppTransform"      -- float32 [4, 4]
    "frameStartTimeStampNs"    -- uint64
    "frameStartPosM"           -- float32 [3]
    "frameStartOrientation"    -- float32 [4]
    "frameEndTimeStampNs"      -- uint64
    "frameEndPosM"             -- float32 [3]
    "frameEndOrientation"      -- float32 [4]
    "maxPoints"                -- uint32     (maximum detection allocation; use Counts for the valid per-frame count)

Field-by-field meaning is in Radar Sensors.