From 2cdd491d7aae4871fa8dc5df4003e69448ca96d3 Mon Sep 17 00:00:00 2001 From: sebastianschroder Date: Tue, 20 Oct 2026 15:00:54 +1100 Subject: [PATCH 1/2] initial commit for numpy updated nuscenes-devkit --- pyproject.toml | 41 +++++ .../get_panoptic_from_seg_det_or_track.py | 7 +- .../utils/tests/test_numpy_compatibility.py | 143 ++++++++++++++++++ setup/requirements/requirements_base.txt | 2 +- 4 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 pyproject.toml create mode 100644 python-sdk/nuscenes/utils/tests/test_numpy_compatibility.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..b4a504ad5 --- /dev/null +++ b/pyproject.toml @@ -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"]} diff --git a/python-sdk/nuscenes/eval/panoptic/get_panoptic_from_seg_det_or_track.py b/python-sdk/nuscenes/eval/panoptic/get_panoptic_from_seg_det_or_track.py index bcbd458a8..fdabf75e0 100644 --- a/python-sdk/nuscenes/eval/panoptic/get_panoptic_from_seg_det_or_track.py +++ b/python-sdk/nuscenes/eval/panoptic/get_panoptic_from_seg_det_or_track.py @@ -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) @@ -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] @@ -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 @@ -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)) diff --git a/python-sdk/nuscenes/utils/tests/test_numpy_compatibility.py b/python-sdk/nuscenes/utils/tests/test_numpy_compatibility.py new file mode 100644 index 000000000..26e3c033b --- /dev/null +++ b/python-sdk/nuscenes/utils/tests/test_numpy_compatibility.py @@ -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() diff --git a/setup/requirements/requirements_base.txt b/setup/requirements/requirements_base.txt index e40e161dd..6c1c0d430 100644 --- a/setup/requirements/requirements_base.txt +++ b/setup/requirements/requirements_base.txt @@ -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 From 28c596dfde6ed280b0feb6c97b1a948fc5be5713 Mon Sep 17 00:00:00 2001 From: Sebastian Schroder <98879113+DARKMOONlite@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:09:46 +1100 Subject: [PATCH 2/2] Update README with local installation instructions Added instructions for local installation of the devkit. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f47c2ce88..5199837bd 100644 --- a/README.md +++ b/README.md @@ -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