Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ Our devkit is available and can be installed via [pip](https://pip.pypa.io/en/st
```
pip install nuscenes-devkit
```
or to build locally
```
pip install .
```

For an advanced installation, see [installation](https://github.com/nutonomy/nuscenes-devkit/blob/master/docs/installation.md) for detailed instructions.

## nuImages
Expand Down
41 changes: 41 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"

[project]
name = "nuscenes-devkit"
version = "1.2.0"
description = "The official devkit of the nuScenes dataset (www.nuscenes.org)."
readme = "README.md"
requires-python = ">=3.9"
license = {text = "apache-2.0"}
authors = [
{name = "Holger Caesar, Oscar Beijbom, Qiang Xu, Varun Bankiti, Alex H. Lang, Sourabh Vora, Venice Erin Liong, Sergi Widjaja, Kiwoo Shin, Caglayan Dicle, Freddy Boulton, Whye Kit Fong, Asha Asvathaman, Lubing Zhou et al.", email = "nuscenes@motional.com"},
]
classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.12",
"Operating System :: OS Independent",
"License :: Free for non-commercial use",
]
dynamic = ["dependencies", "optional-dependencies"]

[project.urls]
Homepage = "https://github.com/nutonomy/nuscenes-devkit"

[tool.setuptools.packages.find]
where = ["python-sdk"]
include = ["nuscenes", "nuscenes.*", "nuimages", "nuimages.*"]

[tool.setuptools.package-data]
"*" = ["*.json"]

[tool.setuptools.dynamic]
dependencies = {file = [
"setup/requirements/requirements_base.txt",
"setup/requirements/requirements_nuimages.txt",
]}

[tool.setuptools.dynamic.optional-dependencies]
prediction = {file = ["setup/requirements/requirements_prediction.txt"]}
tracking = {file = ["setup/requirements/requirements_tracking.txt"]}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def generate_panoptic_labels(nusc: NuScenes,
lidar_path = os.path.join(nusc.dataroot, sd_record['filename'])
lidarseg_pred_filename = os.path.join(lidarseg_preds_folder, 'lidarseg', nusc.version.split('-')[-1],
sd_record['token'] + '_lidarseg.bin')
lidar_seg = LidarSegPointCloud(lidar_path, lidarseg_pred_filename)
lidar_seg:LidarSegPointCloud = LidarSegPointCloud(lidar_path, lidarseg_pred_filename)

panop_labels = np.zeros(lidar_seg.labels.shape, dtype=np.uint16)
overlaps = np.zeros(lidar_seg.labels.shape, dtype=np.uint8)
Expand All @@ -89,7 +89,7 @@ def generate_panoptic_labels(nusc: NuScenes,
# Tracking IDs will be None if pred_boxes is of DetectionBox type.
sorted_pred_boxes, pred_cls, tracking_ids = sort_confidence(pred_boxes)
sorted_pred_boxes = boxes_to_sensor(sorted_pred_boxes, pose_record, cs_record)

# Go through each box (a.k.a instance) and obtain the panoptic label for each.
for instance_id, (pred_box, cl, tracking_id) in enumerate(zip(sorted_pred_boxes, pred_cls, tracking_ids)):
cl_id = coarse2idx[cl]
Expand All @@ -101,6 +101,7 @@ def generate_panoptic_labels(nusc: NuScenes,
inst_tok2id[scene_token][tracking_id] = len(inst_tok2id[scene_token]) + 1

msk = np.zeros(lidar_seg.labels.shape, dtype=np.uint8)

indices = np.where(points_in_box(pred_box, lidar_seg.points[:, :3].T))[0]

msk[indices] = 1
Expand All @@ -120,7 +121,7 @@ def generate_panoptic_labels(nusc: NuScenes,
overlaps += msk

stuff_msk = np.logical_and(panop_labels == 0, lidar_seg.labels >= STUFF_START_COARSE_CLASS_ID)
panop_labels[stuff_msk] = lidar_seg.labels[stuff_msk] * 1000
panop_labels[stuff_msk] = np.uint16(lidar_seg.labels[stuff_msk]) * 1000
panoptic_file = sd_record['token'] + '_panoptic.npz'
np.savez_compressed(os.path.join(panoptic_dir, panoptic_file), data=panop_labels.astype(np.uint16))

Expand Down
143 changes: 143 additions & 0 deletions python-sdk/nuscenes/utils/tests/test_numpy_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""Output regressions for NumPy 2 scalar promotion and default integer changes."""

import os
import tempfile
import unittest
from types import SimpleNamespace
from unittest.mock import Mock, PropertyMock, patch

import numpy as np
from PIL import Image

from nuscenes.eval.detection.data_classes import DetectionBox
from nuscenes.eval.lidarseg.utils import ConfusionMatrix
from nuscenes.eval.panoptic import get_panoptic_from_seg_det_or_track as panoptic
from nuscenes.eval.panoptic.panoptic_seg_evaluator import PanopticEval
from nuscenes.utils.map_mask import MapMask


class TestNumpyCompatibility(unittest.TestCase):

def generate_labels(self, labels, boxes=()):
"""Use real point/label files and geometry; replace only dataset metadata loading."""
with tempfile.TemporaryDirectory() as root:
points = np.zeros((len(labels), 5), dtype=np.float32)
points[:, 0] = np.arange(len(labels))
points.tofile(os.path.join(root, 'points.bin'))
label_dir = os.path.join(root, 'lidarseg', 'mini')
os.makedirs(label_dir)
np.array(labels, dtype=np.uint8).tofile(os.path.join(label_dir, 'lidar_lidarseg.bin'))

identity = {'translation': [0, 0, 0], 'rotation': [1, 0, 0, 0]}
records = {
('sample', 'sample'): {'scene_token': 'scene', 'data': {'LIDAR_TOP': 'lidar'}},
('sample_data', 'lidar'): {
'token': 'lidar', 'filename': 'points.bin',
'calibrated_sensor_token': 'sensor', 'ego_pose_token': 'pose'
},
('calibrated_sensor', 'sensor'): identity,
('ego_pose', 'pose'): identity
}
nusc = SimpleNamespace(
dataroot=root, version='v1.0-mini',
get=Mock(side_effect=lambda table, token: records[table, token])
)
predictions = {'sample': list(boxes)}
with patch.object(panoptic, 'get_samples_in_eval_set', return_value=['sample']), \
patch.object(panoptic, 'PanopticClassMapper') as mapper, \
patch.object(panoptic, 'load_prediction', return_value=(predictions, {})), \
patch.object(panoptic, 'add_center_dist', return_value=predictions):
mapper.return_value.get_coarse2idx.return_value = {'car': 4}
panoptic.generate_panoptic_labels(nusc, root, 'unused.json', 'mini', out_dir=root)

with np.load(os.path.join(root, 'panoptic', 'mini', 'lidar_panoptic.npz')) as saved:
result = saved['data']
self.assertEqual(result.dtype, np.dtype(np.uint16))
self.assertEqual(result.shape, (len(labels),))
return result

def test_panoptic_stuff_uint8_multiplication(self):
"""Python integer 1000 must not be coerced into the uint8 input dtype."""
result = self.generate_labels([0, 4, 11, 12, 13, 14, 15, 16])
np.testing.assert_array_equal(result, [0, 0, 11000, 12000, 13000, 14000, 15000, 16000])

def test_panoptic_mixed_thing_and_stuff(self):
box = DetectionBox(
translation=(0, 0, 0), size=(1, 1, 1), rotation=(1, 0, 0, 0),
detection_name='car', detection_score=0.9
)
result = self.generate_labels([4, 4, 11, 16], [box])
np.testing.assert_array_equal(result, [4001, 0, 11000, 16000])

def test_panoptic_float32_overlap_threshold(self):
"""Exercise float32 count + Python epsilon at and above the overlap threshold."""
first = DetectionBox(
translation=(0.5, 0, 0), size=(1, 1.2, 1), rotation=(1, 0, 0, 0),
detection_name='car', detection_score=0.9
)
for center, expected in [(1.5, [4001, 4001, 4002]), (0.5, [4001, 4001, 0])]:
with self.subTest(center=center):
second = DetectionBox(
translation=(center, 0, 0), size=(1, 1.2, 1), rotation=(1, 0, 0, 0),
detection_name='car', detection_score=0.8
)
np.testing.assert_array_equal(self.generate_labels([4, 4, 4], [first, second]), expected)

def test_panoptic_empty_box_mask(self):
"""A zero float32 count plus epsilon must keep an empty box from producing NaNs."""
box = DetectionBox(
translation=(100, 0, 0), size=(1, 1, 1), rotation=(1, 0, 0, 0),
detection_name='car', detection_score=0.9
)
with np.errstate(divide='raise', invalid='raise'):
result = self.generate_labels([4, 11], [box])
np.testing.assert_array_equal(result, [0, 11000])

def test_panoptic_large_instance_offset(self):
"""The 2**32 Python offset must be applied in a sufficiently wide integer dtype."""
evaluator = PanopticEval(n_classes=2, ignore=[0], min_points=1)
semantics = np.array([[1, 1, 1, 1]], dtype=np.uint8)
predicted = np.array([[1000, 1000, 32000, 32000]], dtype=np.uint16)
target = np.array([[2000, 2000, 31000, 31000]], dtype=np.uint16)
evaluator.addBatch(semantics, predicted, semantics, target)
np.testing.assert_array_equal(evaluator.pan_tp, [0, 2])
np.testing.assert_array_equal(evaluator.pan_fp, [0, 0])
np.testing.assert_array_equal(evaluator.pan_fn, [0, 0])
np.testing.assert_allclose(evaluator.getPQ()[:3], [1, 1, 1], rtol=0, atol=0)

def test_confusion_matrix_uint8_label_promotion(self):
"""Default int width must not let flattened class-pair indices wrap at 255."""
evaluator = ConfusionMatrix(num_classes=32)
evaluator.update(
np.array([31, 31, 1, 1], dtype=np.uint8),
np.array([31, 1, 1, 31], dtype=np.uint8)
)
expected = np.zeros((32, 32), dtype=np.int64)
expected[31, 31] = expected[31, 1] = expected[1, 1] = expected[1, 31] = 1
np.testing.assert_array_equal(evaluator.global_cm, expected)
self.assertEqual(evaluator.global_cm.dtype.kind, 'i')
np.testing.assert_allclose(
evaluator.get_per_class_iou()[[1, 31]], [1 / 3, 1 / 3], rtol=1e-6, atol=0
)

def test_map_dilation_float32_scalar_promotion(self):
"""OpenCV float32 distances multiplied by Python/NumPy scalars must agree."""
with tempfile.TemporaryDirectory() as root:
filename = os.path.join(root, 'map.png')
mask = np.zeros((5, 5), dtype=np.uint8)
mask[2, 2] = 255
Image.fromarray(mask).save(filename)
expected = np.zeros_like(mask)
expected[2, 2] = expected[1, 2] = expected[3, 2] = 255
expected[2, 1] = expected[2, 3] = 255
for resolution in (0.1, np.float32(0.1), np.float64(0.1)):
with self.subTest(resolution_type=type(resolution).__name__):
# Isolate dilation from float32 rounding in the image resize dimensions.
with patch.object(MapMask, '_base_mask', new_callable=PropertyMock, return_value=mask):
result = MapMask(filename, resolution=resolution).mask(dilation=0.11)
self.assertEqual(result.dtype, np.dtype(np.uint8))
np.testing.assert_array_equal(result, expected)


if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion setup/requirements/requirements_base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ cachetools
descartes
fire
matplotlib>=3.6.0
numpy>=1.22.0,<2.0.0
numpy>=1.22.0
opencv-python-headless>=4.5.4.58
Pillow>6.2.1
pyquaternion>=0.9.5
Expand Down