Skip to content

Estimating vertical angle range for every channel in LIDAR #1185

@iyi-alam

Description

@iyi-alam

Hi,
For some personal work, I was trying to figure out the range of vertical angle each channel spans. I wrote a simple script in which for a given point $x,y,z,i,c$ in lidar sensor co-ordinate, where $c$ is channel index, I compute the vertical elevation angle using $$\theta = \arctan{\frac{z}{\sqrt{x^2 + y^2}}}$$ and then keep track of minimum and maximum theta for each channel. However, I got the range -4 degree to +4 almost for every channel, which is definitely wrong given the sensor specs mention the range -

  • field of View (Vertical): +10.67° to -30.67° (41.33°)
  • Angular Resolution (Vertical): 1.33.

Here is result for some of channels -

{
    "0": {
        "min_theta": -4.142869472503662,
        "max_theta": 4.17689847946167
    },
    "1": {
        "min_theta": -4.026743412017822,
        "max_theta": 4.176898002624512
    },
    "2": {
        "min_theta": -4.027475833892822,
        "max_theta": 4.17689847946167
    },
    "3": {
        "min_theta": -4.273198127746582,
        "max_theta": 4.176898002624512
    },
    "4": {
        "min_theta": -4.0284247398376465,
        "max_theta": 4.176896572113037
    },
}

Can someone tell me what is going wrong here? I expected the ranges should go monotonically from -30.67° to +10.67°
Here is my angle estimation code -

import os
import json
import random
import numpy as np
from tqdm import tqdm
from pyquaternion import Quaternion
from nuscenes.nuscenes import NuScenes
from nuscenes.utils.geometry_utils import transform_matrix

DATA_ROOT = "/home/saksham/samsad/mtech-project/datasets/nuscenes"
OUTPUT_DIR = os.path.dirname(os.path.abspath(__file__))
VERSION = "v1.0-mini"
RANDOM_SEED = 42


def load_lidar_with_ring(path):
    points = np.fromfile(path, dtype=np.float32).reshape(-1, 5)
    return points  # x,y,z,intensity,ring


def transform_to_lidar_frame(nusc, token):
    sd = nusc.get("sample_data", token)
    cs = nusc.get("calibrated_sensor", sd["calibrated_sensor_token"])
    ep = nusc.get("ego_pose", sd["ego_pose_token"])

    filepath = os.path.join(DATA_ROOT, sd["filename"])
    pts = load_lidar_with_ring(filepath)

    xyz = pts[:, :3]
    ones = np.ones((xyz.shape[0], 1))
    xyz_h = np.hstack((xyz, ones))

    # Global → ego
    global_from_ego = transform_matrix(ep["translation"], Quaternion(ep["rotation"]), inverse=False)
    ego_from_global = np.linalg.inv(global_from_ego)
    xyz_h = xyz_h @ ego_from_global.T

    # Ego → lidar
    ego_from_lidar = transform_matrix(cs["translation"], Quaternion(cs["rotation"]), inverse=False)
    lidar_from_ego = np.linalg.inv(ego_from_lidar)
    xyz_h = xyz_h @ lidar_from_ego.T

    pts[:, :3] = xyz_h[:, :3]
    return pts


def compute_vertical_angles(xyz):
    x, y, z = xyz[:, 0], xyz[:, 1], xyz[:, 2]
    theta = np.arctan2(z, np.sqrt(x**2 + y**2))
    return np.degrees(theta)


def get_all_lidar_tokens(nusc):
    visited = set()
    infos = []

    for sample in nusc.sample:
        token = sample["data"]["LIDAR_TOP"]

        while token != "":
            sd = nusc.get("sample_data", token)
            fname = sd["filename"]

            if fname not in visited:
                visited.add(fname)
                infos.append((token, fname))

            token = sd["next"]

    return infos


def split_train_test(infos, train_ratio=0.8):
    random.seed(RANDOM_SEED)
    random.shuffle(infos)
    split = int(len(infos) * train_ratio)
    return infos[:split], infos[split:]


def estimate_channel_ranges(nusc, train_infos):
    channel_min = {}
    channel_max = {}

    for token, fname in tqdm(train_infos, desc="Processing train lidar files"):
        pts = transform_to_lidar_frame(nusc, token)
        xyz = pts[:, :3]
        channel = pts[:, 4].astype(int)

        theta = compute_vertical_angles(xyz)

        for ch in np.unique(channel):
            ch_angles = theta[channel == ch]
            if ch not in channel_min:
                channel_min[ch] = np.min(ch_angles)
                channel_max[ch] = np.max(ch_angles)
            else:
                channel_min[ch] = min(channel_min[ch], np.min(ch_angles))
                channel_max[ch] = max(channel_max[ch], np.max(ch_angles))

    return {
        int(ch): {
            "min_theta": float(channel_min[ch]),
            "max_theta": float(channel_max[ch])
        }
        for ch in sorted(channel_min.keys())
    }


def save_split_files(train_infos, test_infos):
    with open(os.path.join(OUTPUT_DIR, "train.txt"), "w") as f:
        for _, fname in train_infos:
            f.write(os.path.basename(fname) + "\n")

    with open(os.path.join(OUTPUT_DIR, "test.txt"), "w") as f:
        for _, fname in test_infos:
            f.write(os.path.basename(fname) + "\n")


def save_json(data):
    with open(os.path.join(OUTPUT_DIR, "channel_angle_ranges.json"), "w") as f:
        json.dump(data, f, indent=4)


def main():
    print("Loading nuScenes...")
    nusc = NuScenes(version=VERSION, dataroot=DATA_ROOT, verbose=True)

    print("Collecting unique lidar sweeps and samples...")
    infos = get_all_lidar_tokens(nusc)
    print(f"Total unique lidar files: {len(infos)}")

    train_infos, test_infos = split_train_test(infos)
    print(f"Train: {len(train_infos)} | Test: {len(test_infos)}")

    save_split_files(train_infos, test_infos)

    print("Estimating per-channel vertical angle ranges...")
    channel_ranges = estimate_channel_ranges(nusc, train_infos)

    save_json(channel_ranges)
    print("Done. Outputs saved in script directory.")


if __name__ == "__main__":
    main()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions