Skip to content

Commit 818cad0

Browse files
authored
Add nuScenes Dataset Support for 2D Object Detection (#397)
* Update tutorial, add nuscenes dataset, update dependencies * Added ontology file creation, cleaning code * Add docstrings to nuscenes_detection and tutorial_image_detection; update nuscenes-devkit in pyproject.toml * Run Black on __init__.py * Added sphinx style doc strings * Relative Paths corrected for image_detection tut * Added nuImages Segmentation and Image Detection Support for tutorials * Added nuImages ontolology config generation in segmentation tutorial * separate nuimages segmentation and detection bild functions and tutorials * Remove redundant cell
1 parent 702c531 commit 818cad0

4 files changed

Lines changed: 1459 additions & 0 deletions

File tree

examples/tutorial_nuimages_detection.ipynb

Lines changed: 873 additions & 0 deletions
Large diffs are not rendered by default.

examples/tutorial_nuimages_segmentation.ipynb

Lines changed: 202 additions & 0 deletions
Large diffs are not rendered by default.

perceptionmetrics/datasets/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from perceptionmetrics.datasets.nuimages import (
2+
NuImagesDetectionDataset,
3+
NuImagesSegmentationDataset,
4+
)
15
from perceptionmetrics.datasets.gaia import (
26
GaiaImageSegmentationDataset,
37
GaiaLiDARSegmentationDataset,
@@ -33,6 +37,8 @@
3337
"rellis3d_lidar_segmentation": Rellis3DLiDARSegmentationDataset,
3438
"rugd_image_segmentation": RUGDImageSegmentationDataset,
3539
"wildscenes_image_segmentation": WildscenesImageSegmentationDataset,
40+
"nuimages_image_segmentation": NuImagesSegmentationDataset,
41+
"nuimages_detection": NuImagesDetectionDataset,
3642
}
3743

3844
if CocoDataset is not None:
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
import os
2+
from typing import Tuple, List, Optional
3+
import pandas as pd
4+
import numpy as np
5+
import cv2
6+
from perceptionmetrics.datasets.detection import ImageDetectionDataset
7+
from nuimages import NuImages
8+
from nuimages.utils.utils import name_to_index_mapping
9+
from perceptionmetrics.datasets import segmentation as segmentation_dataset
10+
11+
DROP = {}
12+
13+
def _get_rgb_from_idx(class_idx: int) -> List[int]:
14+
"""Generate a deterministic RGB color for a class index."""
15+
rng = np.random.default_rng(seed=class_idx + 17)
16+
rgb = rng.integers(low=40, high=256, size=3)
17+
return [int(rgb[0]), int(rgb[1]), int(rgb[2])]
18+
19+
20+
def build_nuimages_detection_dataset(
21+
dataset_dir: str,
22+
version: str = "v1.0-mini",
23+
split: str = "train",
24+
nuim_object: Optional[NuImages] = None,
25+
) -> Tuple[pd.DataFrame, dict]:
26+
"""
27+
Build a nuImages 2D detection dataset index.
28+
29+
Iterates through the nuImages scenes and samples, collects image paths for a given camera, and constructs a dataset index along with a category ontology mapping class names to integer indices.
30+
31+
:param dataset_dir: Path to the nuImages dataset root directory.
32+
:type dataset_dir: str
33+
:param version: nuImages dataset version to load, defaults to "v1.0-mini".
34+
:type version: str
35+
:param split: Dataset split to load ("train" or "val"), defaults to "train".
36+
:type split: str
37+
:param nuim_object: Optional pre-initialized NuImages object to reuse, defaults to None.
38+
:type nuim_object: Optional[NuImages]
39+
:return: Tuple containing:
40+
- A pandas DataFrame with columns ["image", "annotation", "split"] for each sample.
41+
- An ontology dictionary mapping category names to indices.
42+
:rtype: Tuple[pd.DataFrame, dict]
43+
"""
44+
45+
dataset_dir = os.path.abspath(dataset_dir)
46+
assert os.path.isdir(
47+
dataset_dir
48+
), f"Dataset directory {dataset_dir} does not exist."
49+
50+
nuim = (
51+
nuim_object
52+
if nuim_object
53+
else NuImages(version=version, dataroot=dataset_dir, verbose=False)
54+
)
55+
56+
all_categories = [cat["name"] for cat in nuim.category]
57+
categories = [cat for cat in all_categories if cat not in DROP]
58+
59+
ontology = {
60+
name: {"idx": i + 1, "rgb": _get_rgb_from_idx(i + 1)}
61+
for i, name in enumerate(categories)
62+
}
63+
cat_to_idx = {name: ontology[name]["idx"] for name in ontology}
64+
65+
rows = []
66+
for sample in nuim.sample:
67+
key_camera_token = sample["key_camera_token"]
68+
sample_data = nuim.get("sample_data", key_camera_token)
69+
rows.append(
70+
{
71+
"image": os.path.join(dataset_dir, sample_data["filename"]),
72+
"annotation": key_camera_token,
73+
"split": split,
74+
}
75+
)
76+
77+
dataset = pd.DataFrame(rows)
78+
dataset.attrs = {
79+
"ontology": ontology,
80+
"cat_to_idx": cat_to_idx,
81+
}
82+
83+
print(
84+
f"Built nuimages detection dataset with {len(dataset)} samples and "
85+
f"{len(categories)} categories."
86+
)
87+
return dataset, ontology
88+
89+
90+
def build_nuimages_segmentation_dataset(
91+
dataset_dir: str,
92+
version: str = "v1.0-mini",
93+
split: str = "train",
94+
labels_rel_dir: str = "generated/nuimages_segmentation_labels",
95+
nuim_object: Optional[NuImages] = None,
96+
) -> Tuple[pd.DataFrame, dict]:
97+
"""
98+
Build a nuImages semantic segmentation dataset index and masks.
99+
100+
:param dataset_dir: Path to the nuImages dataset root directory.
101+
:type dataset_dir: str
102+
:param version: nuImages dataset version to load, defaults to "v1.0-mini".
103+
:type version: str
104+
:param split: Dataset split to load ("train" or "val"), defaults to "train".
105+
:type split: str
106+
:param labels_rel_dir: Relative directory for segmentation labels, defaults to "generated/nuimages_segmentation_labels".
107+
:type labels_rel_dir: str
108+
:param nuim_object: Optional pre-initialized NuImages object to reuse, defaults to None.
109+
:type nuim_object: Optional[NuImages]
110+
:return: Tuple containing:
111+
- A pandas DataFrame with columns ["image", "label", "split"] for each sample.
112+
- An ontology dictionary mapping category names to indices.
113+
:rtype: Tuple[pd.DataFrame, dict]
114+
"""
115+
116+
dataset_dir = os.path.abspath(dataset_dir)
117+
assert os.path.isdir(
118+
dataset_dir
119+
), f"Dataset directory {dataset_dir} does not exist."
120+
121+
nuim = (
122+
nuim_object
123+
if nuim_object
124+
else NuImages(version=version, dataroot=dataset_dir, verbose=False)
125+
)
126+
127+
## For segmentation, we build semantic masks from surface annotations for keyframe images
128+
129+
labels_root = os.path.join(dataset_dir, labels_rel_dir, version)
130+
os.makedirs(labels_root, exist_ok=True)
131+
132+
name_to_global_idx = name_to_index_mapping(nuim.category)
133+
ontology = {"background": {"idx": 0, "rgb": [0, 0, 0]}}
134+
for category_name, category_idx in sorted(
135+
name_to_global_idx.items(), key=lambda item: item[1]
136+
):
137+
ontology[category_name] = {
138+
"idx": int(category_idx),
139+
"rgb": _get_rgb_from_idx(int(category_idx)),
140+
}
141+
142+
rows = []
143+
for sample in nuim.sample:
144+
key_camera_token = sample["key_camera_token"]
145+
sample_data = nuim.get("sample_data", key_camera_token)
146+
147+
image_abs_path = os.path.join(dataset_dir, sample_data["filename"])
148+
semantic_mask, _ = nuim.get_segmentation(key_camera_token)
149+
label = semantic_mask.astype(np.uint8)
150+
151+
label_abs_path = os.path.join(labels_root, f"{key_camera_token}.png")
152+
cv2.imwrite(label_abs_path, label)
153+
154+
rows.append(
155+
{
156+
"image": image_abs_path,
157+
"label": label_abs_path,
158+
"split": split,
159+
}
160+
)
161+
162+
dataset = pd.DataFrame(rows)
163+
dataset.attrs = {"ontology": ontology}
164+
165+
print(
166+
f"Built nuImages segmentation dataset with {len(dataset)} samples and "
167+
f"{len(ontology)} classes."
168+
)
169+
170+
return dataset, ontology
171+
172+
173+
class NuImagesDetectionDataset(ImageDetectionDataset):
174+
"""
175+
Dataset class for nuImages 2D object detection.
176+
177+
Inherits from ImageDetectionDataset and parses 2D bounding boxes
178+
from nuImages.
179+
180+
:param dataset_dir: Path to the nuImages dataset root.
181+
:type dataset_dir: str
182+
:param version: nuImages version to load, defaults to "v1.0-mini".
183+
:type version: str
184+
:param split: Dataset split ("train" or "val").
185+
:type split: str
186+
"""
187+
188+
def __init__(
189+
self,
190+
dataset_dir: str,
191+
version: str = "v1.0-mini",
192+
split: str = "train",
193+
):
194+
"""
195+
Initialize the nuImages 2D detection dataset.
196+
197+
:param dataset_dir: Path to the nuImages dataset root directory.
198+
:param version: nuImages dataset version to load, defaults to "v1.0-mini".
199+
:param split: Dataset split to load ("train" or "val"), defaults to "train".
200+
"""
201+
self.dataset_dir = dataset_dir
202+
self.split = split
203+
self.nuim = NuImages(dataroot=dataset_dir, version=version)
204+
205+
dataset, ontology = build_nuimages_detection_dataset(
206+
dataset_dir=dataset_dir,
207+
version=version,
208+
split=split,
209+
nuim_object=self.nuim,
210+
)
211+
212+
self.cat_to_idx = dataset.attrs["cat_to_idx"]
213+
self.ann_by_sample_data_token = {}
214+
for ann in self.nuim.object_ann:
215+
sample_data_token = ann["sample_data_token"]
216+
if sample_data_token not in self.ann_by_sample_data_token:
217+
self.ann_by_sample_data_token[sample_data_token] = []
218+
self.ann_by_sample_data_token[sample_data_token].append(ann)
219+
220+
super().__init__(dataset=dataset, dataset_dir=dataset_dir, ontology=ontology)
221+
222+
def read_annotation(self, fname: str) -> Tuple[List[List[float]], List[int]]:
223+
"""
224+
Read annotations for a single nuImages sample and return 2D bounding boxes and class indices.
225+
226+
:param fname: Sample token or filename.
227+
:type fname: str
228+
:return: Tuple containing:
229+
- List of bounding boxes ``[[x1, y1, x2, y2], ...]``.
230+
- List of corresponding class indices.
231+
:rtype: Tuple[List[List[float]], List[int]]
232+
"""
233+
234+
if isinstance(fname, str) and ("/" in fname or "\\" in fname):
235+
fname = os.path.basename(fname)
236+
237+
sample_data = self.nuim.get("sample_data", fname)
238+
image_width = float(sample_data["width"])
239+
image_height = float(sample_data["height"])
240+
241+
boxes_out = []
242+
labels_out = []
243+
244+
for ann in self.ann_by_sample_data_token.get(fname, []):
245+
246+
category_name = self.nuim.get("category", ann["category_token"])["name"]
247+
248+
if category_name in DROP:
249+
continue
250+
251+
if category_name not in self.cat_to_idx:
252+
continue
253+
254+
x1_raw, y1_raw, x2_raw, y2_raw = ann["bbox"]
255+
256+
x1 = max(0.0, min(float(x1_raw), image_width - 1.0))
257+
y1 = max(0.0, min(float(y1_raw), image_height - 1.0))
258+
x2 = max(0.0, min(float(x2_raw), image_width - 1.0))
259+
y2 = max(0.0, min(float(y2_raw), image_height - 1.0))
260+
261+
if x2 <= x1 or y2 <= y1:
262+
continue
263+
264+
boxes_out.append([x1, y1, x2, y2])
265+
labels_out.append(self.cat_to_idx[category_name])
266+
267+
return boxes_out, labels_out
268+
269+
270+
nuimages_detection = NuImagesDetectionDataset
271+
272+
273+
class NuImagesSegmentationDataset(segmentation_dataset.ImageSegmentationDataset):
274+
"""Dataset class for nuImages 2D surface segmentation.
275+
Inherits from ImageSegmentationDataset and constructs semantic segmentation masks
276+
277+
param dataset_dir: Path to the nuImages dataset root.
278+
:type dataset_dir: str
279+
:param version: nuImages version to load, defaults to "v1.0-mini".
280+
:type version: str
281+
:param split: Dataset split ("train" or "val").
282+
:type split: str
283+
:param labels_rel_dir: Relative directory within the dataset where generated segmentation label images will be stored, defaults to "generated/nuimages_segmentation_labels".
284+
:type labels_rel_dir: str
285+
286+
"""
287+
288+
def __init__(
289+
self,
290+
dataset_dir: str,
291+
version: str = "v1.0-mini",
292+
split: str = "train",
293+
labels_rel_dir: str = "generated/nuimages_segmentation_labels",
294+
):
295+
dataset_dir = os.path.abspath(dataset_dir)
296+
assert os.path.isdir(
297+
dataset_dir
298+
), f"Dataset directory {dataset_dir} does not exist."
299+
300+
self.nuim = NuImages(dataroot=dataset_dir, version=version)
301+
dataset, ontology = build_nuimages_segmentation_dataset(
302+
dataset_dir=dataset_dir,
303+
version=version,
304+
split=split,
305+
labels_rel_dir=labels_rel_dir,
306+
nuim_object=self.nuim,
307+
)
308+
309+
super().__init__(dataset, dataset_dir, ontology)
310+
311+
312+
313+
import matplotlib.pyplot as plt
314+
315+
if __name__ == "__main__":
316+
dataset_dir = "local/data/nuimages-v1.0-mini"
317+
version = "v1.0-mini"
318+
split = "train"
319+
dataset = NuImagesSegmentationDataset(dataset_dir, version, split)
320+
321+
sample = dataset.dataset.iloc[10]
322+
label_path = sample["label"]
323+
img_path = sample["image"]
324+
325+
# Load images
326+
img = cv2.imread(img_path)
327+
label_img = cv2.imread(label_path, cv2.IMREAD_GRAYSCALE)
328+
329+
bright_colors = [
330+
[255, 0, 0], # red
331+
[0, 255, 0], # green
332+
[0, 0, 255], # blue
333+
[255, 255, 0], # yellow
334+
[255, 0, 255], # magenta
335+
[0, 255, 255], # cyan
336+
[255, 128, 0], # orange
337+
[128, 0, 255], # purple
338+
[0, 128, 255], # sky blue
339+
[128, 255, 0], # lime
340+
[255, 0, 128], # pink
341+
[0, 255, 128], # teal
342+
]
343+
344+
# Create color mask
345+
color_mask = np.zeros_like(img)
346+
for class_name, class_info in dataset.ontology.items():
347+
class_idx = class_info["idx"]
348+
rgb = bright_colors[class_idx % len(bright_colors)]
349+
color_mask[label_img == class_idx] = rgb
350+
351+
# Blend original image with color mask
352+
overlay = cv2.addWeighted(img, 0.5, color_mask, 0.5, 0)
353+
354+
# Draw class names
355+
for class_name, class_info in dataset.ontology.items():
356+
class_idx = class_info["idx"]
357+
positions = np.argwhere(label_img == class_idx)
358+
if positions.shape[0] == 0:
359+
continue
360+
y, x = np.mean(positions, axis=0).astype(int)
361+
cv2.putText(
362+
overlay,
363+
class_name,
364+
(x, y),
365+
cv2.FONT_HERSHEY_SIMPLEX,
366+
0.5,
367+
(255, 255, 255),
368+
1,
369+
cv2.LINE_AA,
370+
)
371+
372+
# Show overlay
373+
plt.figure(figsize=(12, 8))
374+
plt.imshow(cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB))
375+
plt.axis("off")
376+
plt.title("Image with Segmentation Overlay and Labels")
377+
plt.show()
378+

0 commit comments

Comments
 (0)