Skip to content

Commit fd810f7

Browse files
authored
Add Cityscapes dataset adapter (#577)
* Add Citscapes dataset adapter * Add Cityscapes dataset adapter and cleanup * Update datasets/__init__.py * Correction in build_dataset arg * Add helper translation function * Black formatting, add cityscapescripts dependency * Remove poetry lock file * Revert pyproject dependency change * Restore cityscacpescripts dependency
1 parent 789f9f1 commit fd810f7

4 files changed

Lines changed: 237 additions & 4231 deletions

File tree

perceptionmetrics/datasets/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
)
2121
from perceptionmetrics.datasets.rugd import RUGDImageSegmentationDataset
2222
from perceptionmetrics.datasets.wildscenes import WildscenesImageSegmentationDataset
23+
from perceptionmetrics.datasets.cityscapes import CityscapesImageSegmentationDataset
24+
2325
try:
2426
from perceptionmetrics.datasets.coco import CocoDataset
2527
except ImportError:
2628
print("COCO dataset dependencies not available")
2729
CocoDataset = None
2830

31+
2932
REGISTRY = {
3033
"gaia_image_segmentation": GaiaImageSegmentationDataset,
3134
"gaia_lidar_segmentation": GaiaLiDARSegmentationDataset,
@@ -37,9 +40,10 @@
3740
"rellis3d_lidar_segmentation": Rellis3DLiDARSegmentationDataset,
3841
"rugd_image_segmentation": RUGDImageSegmentationDataset,
3942
"wildscenes_image_segmentation": WildscenesImageSegmentationDataset,
43+
"cityscapes_image_segmentation": CityscapesImageSegmentationDataset,
4044
"nuimages_image_segmentation": NuImagesSegmentationDataset,
4145
"nuimages_detection": NuImagesDetectionDataset,
4246
}
4347

4448
if CocoDataset is not None:
45-
REGISTRY["coco_image_detection"] = CocoDataset
49+
REGISTRY["coco_image_detection"] = CocoDataset
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
from collections import OrderedDict
2+
from glob import glob
3+
import json
4+
import os
5+
from typing import Optional, Tuple
6+
7+
import pandas as pd
8+
9+
from perceptionmetrics.datasets import segmentation as segmentation_dataset
10+
from cityscapesscripts.helpers.labels import labels
11+
12+
13+
def build_dataset_ontology(
14+
use_train_id: bool = False, ontology_fname: Optional[str] = None
15+
) -> dict:
16+
"""Build ontology dictionary from Cityscapes dataset labels
17+
18+
:param use_train_id: Whether to use train IDs instead of Cityscapes label IDs, defaults to False
19+
:type use_train_id: bool, optional
20+
:param ontology_fname: Optional JSON file path where the ontology should be saved, defaults to None
21+
:type ontology_fname: Optional[str], optional
22+
:return: Ontology dictionary mapping class names to label metadata
23+
:rtype: dict
24+
"""
25+
ontology = {}
26+
for label in labels:
27+
if label.ignoreInEval:
28+
continue
29+
ontology[label.name] = {
30+
"idx": label.trainId if use_train_id else label.id,
31+
"train_id": label.trainId,
32+
"cityscapes_id": label.id,
33+
"category": label.category,
34+
"category_id": label.categoryId,
35+
"has_instances": label.hasInstances,
36+
"rgb": label.color,
37+
}
38+
39+
if ontology_fname is not None:
40+
ontology_dir = os.path.dirname(ontology_fname)
41+
if ontology_dir:
42+
os.makedirs(ontology_dir, exist_ok=True)
43+
with open(ontology_fname, "w", encoding="utf-8") as f:
44+
json.dump(ontology, f, indent=2)
45+
46+
return ontology
47+
48+
49+
def build_train_id_ontology_translation() -> dict:
50+
"""Build ontology translation from Cityscapes label IDs to train IDs.
51+
52+
:return: Translation dictionary mapping raw Cityscapes class names to train ID class names
53+
:rtype: dict
54+
"""
55+
return {label.name: label.name for label in labels if not label.ignoreInEval}
56+
57+
58+
def build_dataset(
59+
train_dataset_root: Optional[str] = None,
60+
val_dataset_root: Optional[str] = None,
61+
test_dataset_root: Optional[str] = None,
62+
image_dir: str = "leftImg8bit_trainvaltest/leftImg8bit",
63+
label_dir: str = "gtFine",
64+
image_suffix: str = "_leftImg8bit.png",
65+
label_suffix: str = "_gtFine_labelIds.png",
66+
use_train_id: bool = False,
67+
) -> Tuple[dict, dict]:
68+
"""Build dataset and ontology dictionaries from Cityscapes dataset structure
69+
70+
:param train_dataset_root: Root directory containing training data, defaults to None
71+
:type train_dataset_root: str, optional
72+
:param val_dataset_root: Root directory containing validation data, defaults to None
73+
:type val_dataset_root: str, optional
74+
:param test_dataset_root: Root directory containing test data, defaults to None
75+
:type test_dataset_root: str, optional
76+
:param image_dir: Subdirectory containing images within each dataset directory, defaults to "leftImg8bit_trainvaltest/leftImg8bit"
77+
:type image_dir: str, optional
78+
:param label_dir: Subdirectory containing labels within each dataset directory, defaults to "gtFine"
79+
:type label_dir: str, optional
80+
:param image_suffix: File suffix used to filter image files, defaults to "_leftImg8bit.png"
81+
:type image_suffix: str, optional
82+
:param label_suffix: File suffix used to filter label files, defaults to "_gtFine_labelIds.png"
83+
:type label_suffix: str, optional
84+
:param use_train_id: Whether to use train IDs instead of Cityscapes label IDs, defaults to False
85+
:type use_train_id: bool, optional
86+
:return: Dataset and ontology dictionaries
87+
:rtype: Tuple[dict, dict]
88+
"""
89+
dataset_dirs = {
90+
"train": train_dataset_root,
91+
"val": val_dataset_root,
92+
"test": test_dataset_root,
93+
}
94+
dataset_dirs = {
95+
split: os.path.abspath(d) for split, d in dataset_dirs.items() if d is not None
96+
}
97+
if not dataset_dirs:
98+
raise ValueError("At least one dataset directory must be provided")
99+
100+
if use_train_id and label_suffix == "_gtFine_labelIds.png":
101+
raise ValueError(
102+
"use_train_id=True requires train-id labels. Set "
103+
"label_suffix='_gtFine_labelTrainIds.png' or export the dataset to a "
104+
"train-id ontology before evaluating train-id models."
105+
)
106+
107+
ontology = build_dataset_ontology(use_train_id=use_train_id)
108+
dataset = OrderedDict()
109+
110+
for split, dataset_dir in dataset_dirs.items():
111+
112+
image_pattern = os.path.join(
113+
dataset_dir,
114+
image_dir,
115+
split,
116+
"*",
117+
f"*{image_suffix}",
118+
)
119+
120+
image_fnames = sorted(glob(image_pattern))
121+
122+
if len(image_fnames) == 0:
123+
print(f"No images found for split={split}")
124+
print(f"Searched pattern: {image_pattern}")
125+
continue
126+
127+
for image_fname in image_fnames:
128+
image_fname = os.path.abspath(image_fname)
129+
130+
city = os.path.basename(os.path.dirname(image_fname))
131+
image_basename = os.path.basename(image_fname)
132+
133+
sample_name = image_basename.replace(image_suffix, "")
134+
135+
label_basename = f"{sample_name}{label_suffix}"
136+
137+
label_fname = os.path.join(
138+
dataset_dir,
139+
label_dir,
140+
split,
141+
city,
142+
label_basename,
143+
)
144+
label_fname = os.path.abspath(label_fname)
145+
146+
if not os.path.exists(label_fname):
147+
print(f"Missing label for image: {image_fname}")
148+
print(f"Expected label path: {label_fname}")
149+
continue
150+
151+
dataset[sample_name] = (
152+
image_fname,
153+
label_fname,
154+
city,
155+
split,
156+
)
157+
158+
return dataset, ontology
159+
160+
161+
class CityscapesImageSegmentationDataset(segmentation_dataset.ImageSegmentationDataset):
162+
"""Specific class for Cityscapes-styled image segmentation datasets. The dataset can be
163+
downloaded from the official webpage (https://www.cityscapes-dataset.com):
164+
images -> leftImg8bit_trainvaltest.zip
165+
labels -> gtFine_trainvaltest.zip
166+
167+
:param train_dataset_root: Root directory containing training data, defaults to None
168+
:type train_dataset_root: str, optional
169+
:param val_dataset_root: Root directory containing validation data, defaults to None
170+
:type val_dataset_root: str, optional
171+
:param test_dataset_root: Root directory containing test data, defaults to None
172+
:type test_dataset_root: str, optional
173+
:param image_dir: Subdirectory containing images within each dataset directory, defaults to "leftImg8bit_trainvaltest/leftImg8bit"
174+
:type image_dir: str, optional
175+
:param label_dir: Subdirectory containing labels within each dataset directory, defaults to "gtFine"
176+
:type label_dir: str, optional
177+
:param image_suffix: File suffix used to filter image files, defaults to "_leftImg8bit.png"
178+
:type image_suffix: str, optional
179+
:param label_suffix: File suffix used to filter label files, defaults to "_gtFine_labelIds.png"
180+
:type label_suffix: str, optional
181+
:param use_train_id: Whether to use train IDs instead of Cityscapes label IDs, defaults to False
182+
:type use_train_id: bool, optional
183+
"""
184+
185+
def __init__(
186+
self,
187+
train_dataset_root: Optional[str] = None,
188+
val_dataset_root: Optional[str] = None,
189+
test_dataset_root: Optional[str] = None,
190+
image_dir: str = "leftImg8bit_trainvaltest/leftImg8bit",
191+
label_dir: str = "gtFine",
192+
image_suffix: str = "_leftImg8bit.png",
193+
label_suffix: str = "_gtFine_labelIds.png",
194+
use_train_id: bool = False,
195+
):
196+
dataset, ontology = build_dataset(
197+
train_dataset_root=train_dataset_root,
198+
val_dataset_root=val_dataset_root,
199+
test_dataset_root=test_dataset_root,
200+
image_dir=image_dir,
201+
label_dir=label_dir,
202+
image_suffix=image_suffix,
203+
label_suffix=label_suffix,
204+
use_train_id=use_train_id,
205+
)
206+
207+
cols = ["image", "label", "scene", "split"]
208+
dataset = pd.DataFrame.from_dict(dataset, orient="index", columns=cols)
209+
210+
if len(dataset) == 0:
211+
raise ValueError(
212+
"No Cityscapes samples were found. Please check dataset paths."
213+
)
214+
215+
print(f"Samples retrieved: {len(dataset)}")
216+
217+
all_dataset_dirs = [train_dataset_root, val_dataset_root, test_dataset_root]
218+
dataset_dir = [d for d in all_dataset_dirs if d is not None][0]
219+
220+
super().__init__(dataset, dataset_dir, ontology)
221+
222+
223+
if __name__ == "__main__":
224+
cityscapes_dir = "local/data/cityscapes"
225+
226+
dataset = CityscapesImageSegmentationDataset(
227+
train_dataset_root=cityscapes_dir,
228+
val_dataset_root=cityscapes_dir,
229+
)
230+
231+
print(dataset.dataset.head())

0 commit comments

Comments
 (0)