Repository: airctic/icevision

Issue:
ClassMap with non-sequential IDs
How to handle non-sequential IDs? Example:

```python
{1: bike, 2: car, 5: motorcycle}
```

### Suggestion 1
The user can only pass a list of labels to the `ClassMap`, it's the job of the user to fill the missing information:
```
[bike, car, None, None, motorcycle]
```

We only need to modify the coco parser for this (so it returns 90 classes instead of 80). Torchvision models and efficientdet expects coco to have 90 classes.

### Suggestion 2
Enable to receive a dict on the `ClassMap` constructor and automatically fill the missing blanks. Checks needs to be implement to make sure the keys of the dict are ints.


Discussion:
I'd opt for the Suggestion 1: what you see is what you get (+background :D)

The user can create a list from a non-sequential dictionary keys using `.values()` or `enumerate`

Resolving patch:
diff --git a/icevision/parsers/coco_parser.py b/icevision/parsers/coco_parser.py
--- a/icevision/parsers/coco_parser.py
+++ b/icevision/parsers/coco_parser.py
@@ -42,9 +42,13 @@ def __init__(
         self._imageid2info = {o["id"]: o for o in self.annotations_dict["images"]}
 
         categories = self.annotations_dict["categories"]
-        self._cocoid2name = {o["id"]: o["name"] for o in categories}
-        self._cocoid2name[0] = BACKGROUND
-        self.class_map = ClassMap(self._cocoid2name.values())
+        id2class = {o["id"]: o["name"] for o in categories}
+        id2class[0] = BACKGROUND
+        # coco has non sequential ids, we fill the blanks with `None`, check #668 for more info
+        classes = [None for _ in range(max(id2class.keys()) + 1)]
+        for i, name in id2class.items():
+            classes[i] = name
+        self.class_map = ClassMap(classes)
 
         super().__init__(record=self.template_record(), idmap=idmap)
 
@@ -76,8 +80,8 @@ def filepath(self, o) -> Path:
     def img_size(self, o) -> ImgSize:
         return get_img_size(self.filepath(o))
 
-    def labels(self, o) -> List[Hashable]:
-        return [self._cocoid2name[o["category_id"]]]
+    def labels_ids(self, o) -> List[Hashable]:
+        return [o["category_id"]]
 
     def areas(self, o) -> List[float]:
         return [o["area"]]
@@ -91,7 +95,7 @@ def parse_fields(self, o, record):
 
         # TODO: is class_map still a issue here?
         record.detect.set_class_map(self.class_map)
-        record.detect.add_labels(self.labels(o))
+        record.detect.add_labels_by_id(self.labels_ids(o))
         record.detect.add_areas(self.areas(o))
         record.detect.add_iscrowds(self.iscrowds(o))
 
@@ -141,10 +145,10 @@ def keypoints(self, o) -> List[KeyPoints]:
             else []
         )
 
-    def labels(self, o) -> List[Hashable]:
+    def labels_ids(self, o) -> List[Hashable]:
         if sum(o["keypoints"]) <= 0:
             return []
-        return super().labels(o)
+        return super().labels_ids(o)
 
     def areas(self, o) -> List[float]:
         return [o["area"]] if sum(o["keypoints"]) > 0 else []
diff --git a/icevision/parsers/parser.py b/icevision/parsers/parser.py
--- a/icevision/parsers/parser.py
+++ b/icevision/parsers/parser.py
@@ -80,13 +80,12 @@ def parse_dicted(self, show_pbar: bool = True) -> Dict[int, RecordType]:
                     record = records[imageid]
                 except KeyError:
                     record = self.create_record()
+                    # HACK: fix imageid (needs to be transformed with idmap)
+                    record.set_imageid(imageid)
+                    records[imageid] = record
 
                 self.parse_fields(sample, record)
 
-                # HACK: fix imageid (needs to be transformed with idmap)
-                record.set_imageid(imageid)
-                records[imageid] = record
-
             except AbortParseRecord as e:
                 logger.warning(
                     "Record with imageid: {} was skipped because: {}",


Test patch:
diff --git a/tests/parsers/test_coco_parser.py b/tests/parsers/test_coco_parser.py
--- a/tests/parsers/test_coco_parser.py
+++ b/tests/parsers/test_coco_parser.py
@@ -34,6 +34,8 @@ def test_bbox_parser(coco_dir, coco_bbox_parser):
     assert record.width == 640
     assert record.height == 480
 
+    assert len(record.detect.class_map) == 91
+    assert record.detect.class_map.get_by_id(90) == "toothbrush"
     assert record.detect.labels == [4]
     assert pytest.approx(record.detect.bboxes[0].xyxy) == (
         175.14,