Skip to content

Commit

Permalink
Merge pull request #4884 from voxel51/bugfix/iss-4615-4795
Browse files Browse the repository at this point in the history
Fix COCO category IDs
  • Loading branch information
brimoor authored Oct 4, 2024
2 parents b481bf3 + 221d807 commit 17e8e53
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 57 deletions.
41 changes: 19 additions & 22 deletions docs/source/integrations/coco.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,18 +192,14 @@ file containing COCO-formatted labels to work with:
dataset = foz.load_zoo_dataset("quickstart")
# Classes list
classes = dataset.distinct("ground_truth.detections.label")
# The directory in which the dataset's images are stored
IMAGES_DIR = os.path.dirname(dataset.first().filepath)
# Export some labels in COCO format
dataset.take(5).export(
dataset.take(5, seed=51).export(
dataset_type=fo.types.COCODetectionDataset,
label_field="ground_truth",
labels_path="/tmp/coco.json",
classes=classes,
)
Now we have a ``/tmp/coco.json`` file on disk containing COCO labels
Expand All @@ -220,7 +216,7 @@ corresponding to the images in ``IMAGES_DIR``:
"licenses": [],
"categories": [
{
"id": 0,
"id": 1,
"name": "airplane",
"supercategory": null
},
Expand All @@ -229,9 +225,9 @@ corresponding to the images in ``IMAGES_DIR``:
"images": [
{
"id": 1,
"file_name": "001631.jpg",
"height": 612,
"width": 612,
"file_name": "003486.jpg",
"height": 427,
"width": 640,
"license": null,
"coco_url": null
},
Expand All @@ -241,14 +237,14 @@ corresponding to the images in ``IMAGES_DIR``:
{
"id": 1,
"image_id": 1,
"category_id": 9,
"category_id": 1,
"bbox": [
92.14,
220.04,
519.86,
61.89000000000001
34.34,
147.46,
492.69,
192.36
],
"area": 32174.135400000006,
"area": 94773.8484,
"iscrowd": 0
},
...
Expand All @@ -271,8 +267,9 @@ dataset:
include_id=True,
)
# Verify that the class list for our dataset was imported
print(coco_dataset.default_classes) # ['airplane', 'apple', ...]
# COCO categories are also imported
print(coco_dataset.info["categories"])
# [{'id': 1, 'name': 'airplane', 'supercategory': None}, ...]
print(coco_dataset)
Expand Down Expand Up @@ -319,16 +316,16 @@ to add them to your dataset as follows:
#
# Mock COCO predictions, where:
# - `image_id` corresponds to the `coco_id` field of `coco_dataset`
# - `category_id` corresponds to classes in `coco_dataset.default_classes`
# - `category_id` corresponds to `coco_dataset.info["categories"]`
#
predictions = [
{"image_id": 1, "category_id": 18, "bbox": [258, 41, 348, 243], "score": 0.87},
{"image_id": 2, "category_id": 11, "bbox": [61, 22, 504, 609], "score": 0.95},
{"image_id": 1, "category_id": 2, "bbox": [258, 41, 348, 243], "score": 0.87},
{"image_id": 2, "category_id": 4, "bbox": [61, 22, 504, 609], "score": 0.95},
]
categories = coco_dataset.info["categories"]
# Add COCO predictions to `predictions` field of dataset
classes = coco_dataset.default_classes
fouc.add_coco_labels(coco_dataset, "predictions", predictions, classes)
fouc.add_coco_labels(coco_dataset, "predictions", predictions, categories)
# Verify that predictions were added to two images
print(coco_dataset.count("predictions")) # 2
Expand Down
5 changes: 2 additions & 3 deletions docs/source/user_guide/dataset_creation/datasets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1499,9 +1499,8 @@ where `labels.json` is a JSON file in the following format:
...
],
"categories": [
...
{
"id": 2,
"id": 1,
"name": "cat",
"supercategory": "animal",
"keypoints": ["nose", "head", ...],
Expand All @@ -1524,7 +1523,7 @@ where `labels.json` is a JSON file in the following format:
{
"id": 1,
"image_id": 1,
"category_id": 2,
"category_id": 1,
"bbox": [260, 177, 231, 199],
"segmentation": [...],
"keypoints": [224, 226, 2, ...],
Expand Down
5 changes: 2 additions & 3 deletions docs/source/user_guide/export_datasets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1646,9 +1646,8 @@ where `labels.json` is a JSON file in the following format:
},
"licenses": [],
"categories": [
...
{
"id": 2,
"id": 1,
"name": "cat",
"supercategory": "animal"
},
Expand All @@ -1669,7 +1668,7 @@ where `labels.json` is a JSON file in the following format:
{
"id": 1,
"image_id": 1,
"category_id": 2,
"category_id": 1,
"bbox": [260, 177, 231, 199],
"segmentation": [...],
"score": 0.95,
Expand Down
62 changes: 34 additions & 28 deletions fiftyone/utils/coco.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def add_coco_labels(
sample_collection,
label_field,
labels_or_path,
classes,
categories,
label_type="detections",
coco_id_field=None,
include_annotation_id=False,
Expand All @@ -68,7 +68,7 @@ def add_coco_labels(
{
"id": 1,
"image_id": 1,
"category_id": 2,
"category_id": 1,
"bbox": [260, 177, 231, 199],
# optional
Expand All @@ -88,7 +88,7 @@ def add_coco_labels(
{
"id": 1,
"image_id": 1,
"category_id": 2,
"category_id": 1,
"bbox": [260, 177, 231, 199],
"segmentation": [...],
Expand All @@ -109,7 +109,7 @@ def add_coco_labels(
{
"id": 1,
"image_id": 1,
"category_id": 2,
"category_id": 1,
"keypoints": [224, 226, 2, ...],
"num_keypoints": 10,
Expand All @@ -129,8 +129,14 @@ def add_coco_labels(
will be created if necessary
labels_or_path: a list of COCO annotations or the path to a JSON file
containing such data on disk
classes: the list of class label strings or a dict mapping class IDs to
class labels
categories: can be any of the following:
- a list of category dicts in the format of
:meth:`parse_coco_categories` specifying the classes and their
category IDs
- a dict mapping class IDs to class labels
- a list of class labels whose 1-based ordering is assumed to
correspond to the category IDs in the provided COCO labels
label_type ("detections"): the type of labels to load. Supported values
are ``("detections", "segmentations", "keypoints")``
coco_id_field (None): this parameter determines how to map the
Expand Down Expand Up @@ -195,10 +201,14 @@ class labels
view.compute_metadata()
widths, heights = view.values(["metadata.width", "metadata.height"])

if isinstance(classes, dict):
classes_map = classes
if isinstance(categories, dict):
classes_map = categories
elif not categories:
classes_map = {}
elif isinstance(categories[0], dict):
classes_map = {c["id"]: c["name"] for c in categories}
else:
classes_map = {i: label for i, label in enumerate(classes)}
classes_map = {i: label for i, label in enumerate(categories, 1)}

labels = []
for _coco_objects, width, height in zip(coco_objects, widths, heights):
Expand Down Expand Up @@ -563,15 +573,11 @@ def setup(self):
self.labels_path, extra_attrs=self.extra_attrs
)

classes = None
if classes_map is not None:
classes = _to_classes(classes_map)

if classes is not None:
info["classes"] = classes
info["classes"] = _to_classes(classes_map)

image_ids = _get_matching_image_ids(
classes,
classes_map,
images,
annotations,
image_ids=self.image_ids,
Expand Down Expand Up @@ -907,12 +913,11 @@ def export_sample(self, image_or_path, label, metadata=None):

def close(self, *args):
if self._dynamic_classes:
classes = sorted(self._classes)
labels_map_rev = _to_labels_map_rev(classes)
labels_map_rev = _to_labels_map_rev(sorted(self._classes))
for anno in self._annotations:
anno["category_id"] = labels_map_rev[anno["category_id"]]
else:
classes = self.classes
elif self.categories is None:
labels_map_rev = _to_labels_map_rev(self.classes)

_info = self.info or {}
_date_created = datetime.now().replace(microsecond=0).isoformat()
Expand All @@ -933,10 +938,10 @@ def close(self, *args):
categories = [
{
"id": i,
"name": l,
"name": c,
"supercategory": None,
}
for i, l in enumerate(classes)
for c, i in sorted(labels_map_rev.items(), key=lambda t: t[1])
]

labels = {
Expand Down Expand Up @@ -1681,7 +1686,7 @@ def download_coco_dataset_split(
if classes is not None:
# Filter by specified classes
all_ids, any_ids = _get_images_with_classes(
image_ids, annotations, classes, all_classes
image_ids, annotations, classes, all_classes_map
)
else:
all_ids = image_ids
Expand Down Expand Up @@ -1846,7 +1851,7 @@ def _parse_include_license(include_license):


def _get_matching_image_ids(
all_classes,
classes_map,
images,
annotations,
image_ids=None,
Expand All @@ -1862,7 +1867,7 @@ def _get_matching_image_ids(

if classes is not None:
all_ids, any_ids = _get_images_with_classes(
image_ids, annotations, classes, all_classes
image_ids, annotations, classes, classes_map
)
else:
all_ids = image_ids
Expand Down Expand Up @@ -1930,7 +1935,7 @@ def _do_download(args):


def _get_images_with_classes(
image_ids, annotations, target_classes, all_classes
image_ids, annotations, target_classes, classes_map
):
if annotations is None:
logger.warning("Dataset is unlabeled; ignoring classes requirement")
Expand All @@ -1939,11 +1944,12 @@ def _get_images_with_classes(
if etau.is_str(target_classes):
target_classes = [target_classes]

bad_classes = [c for c in target_classes if c not in all_classes]
labels_map_rev = {c: i for i, c in classes_map.items()}

bad_classes = [c for c in target_classes if c not in labels_map_rev]
if bad_classes:
raise ValueError("Unsupported classes: %s" % bad_classes)

labels_map_rev = _to_labels_map_rev(all_classes)
class_ids = {labels_map_rev[c] for c in target_classes}

all_ids = []
Expand Down Expand Up @@ -2029,7 +2035,7 @@ def _load_image_ids_json(json_path):


def _to_labels_map_rev(classes):
return {c: i for i, c in enumerate(classes)}
return {c: i for i, c in enumerate(classes, 1)}


def _to_classes(classes_map):
Expand Down
Loading

0 comments on commit 17e8e53

Please sign in to comment.