Repository: openvinotoolkit/datumaro

Issue:
Import for VoTT dataset format
There are two [Visual Object Tagging Tool](https://github.com/microsoft/VoTT) formats: [VoTT JSON](https://roboflow.com/formats/vott-json) and [VoTT CSV](https://roboflow.com/formats/vott-csv). It is a detection task.

Annotation types of VoTT JSON:
- Bounding boxes

Annotation types of VoTT CSV:
- Bounding boxes


Discussion:

Resolving patch:
diff --git a/datumaro/plugins/vott_csv_format.py b/datumaro/plugins/vott_csv_format.py
new file mode 100644
--- /dev/null
+++ b/datumaro/plugins/vott_csv_format.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2021 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import csv
+import os.path as osp
+
+from datumaro.components.annotation import AnnotationType, Bbox, LabelCategories
+from datumaro.components.extractor import DatasetItem, Importer, SourceExtractor
+from datumaro.components.format_detection import FormatDetectionContext
+from datumaro.components.media import Image
+from datumaro.util.meta_file_util import has_meta_file, parse_meta_file
+
+
+class VottCsvPath:
+    ANNO_FILE_SUFFIX = '-export.csv'
+
+class VottCsvExtractor(SourceExtractor):
+    def __init__(self, path):
+        if not osp.isfile(path):
+            raise FileNotFoundError("Can't read annotation file '%s'" % path)
+
+        super().__init__(subset=osp.splitext(osp.basename(path))[0].
+            rsplit('-', maxsplit=1)[0])
+
+        if has_meta_file(path):
+            self._categories = { AnnotationType.label: LabelCategories.
+                from_iterable(parse_meta_file(path).keys()) }
+        else:
+            self._categories = { AnnotationType.label: LabelCategories() }
+
+        self._items = list(self._load_items(path).values())
+
+    def _load_items(self, path):
+        items = {}
+
+        label_categories = self._categories[AnnotationType.label]
+
+        with open(path, encoding='utf-8') as content:
+            for row in csv.DictReader(content):
+                item_id = osp.splitext(row['image'])[0]
+
+                if item_id not in items:
+                    items[item_id] = DatasetItem(id=item_id, subset=self._subset,
+                        image=Image(path=osp.join(osp.dirname(path), row['image'])))
+
+                annotations = items[item_id].annotations
+
+                label_name = row.get('label')
+                x_min = row.get('xmin')
+                y_min = row.get('ymin')
+                x_max = row.get('xmax')
+                y_max = row.get('ymax')
+
+                if label_name and x_min and y_min and x_max and y_max:
+                    label_idx = label_categories.find(label_name)[0]
+                    if label_idx is None:
+                        label_idx = label_categories.add(label_name)
+
+                    x_min = float(x_min)
+                    y_min = float(y_min)
+                    w = float(x_max) - x_min
+                    h = float(y_max) - y_min
+
+                    annotations.append(Bbox(x_min, y_min, w, h, label=label_idx))
+
+        return items
+
+class VottCsvImporter(Importer):
+    @classmethod
+    def find_sources(cls, path):
+        return cls._find_sources_recursive(path, '.csv', 'vott_csv')
+
+    @classmethod
+    def detect(cls, context: FormatDetectionContext) -> None:
+        context.require_file('*' + VottCsvPath.ANNO_FILE_SUFFIX)
diff --git a/datumaro/plugins/vott_json_format.py b/datumaro/plugins/vott_json_format.py
new file mode 100644
--- /dev/null
+++ b/datumaro/plugins/vott_json_format.py
@@ -0,0 +1,84 @@
+# Copyright (C) 2021 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import json
+import os.path as osp
+
+from datumaro.components.annotation import AnnotationType, Bbox, LabelCategories
+from datumaro.components.extractor import DatasetItem, Importer, SourceExtractor
+from datumaro.components.format_detection import FormatDetectionContext
+from datumaro.components.media import Image
+from datumaro.util.meta_file_util import has_meta_file, parse_meta_file
+
+
+class VottJsonPath:
+    ANNO_FILE_SUFFIX = '-export.json'
+
+class VottJsonExtractor(SourceExtractor):
+    def __init__(self, path):
+        if not osp.isfile(path):
+            raise FileNotFoundError("Can't read annotation file '%s'" % path)
+
+        super().__init__(subset=osp.splitext(osp.basename(path))[0].
+            rsplit('-', maxsplit=1)[0])
+
+        if has_meta_file(path):
+            self._categories = { AnnotationType.label: LabelCategories.
+                from_iterable(parse_meta_file(path).keys()) }
+        else:
+            self._categories = { AnnotationType.label: LabelCategories() }
+
+        self._items = list(self._load_items(path).values())
+
+    def _load_items(self, path):
+        items = {}
+
+        with open(path) as f:
+            anno_dict = json.load(f)
+
+        label_categories = self._categories[AnnotationType.label]
+        tags = anno_dict.get('tags', [])
+        for label in tags:
+            label_name = label.get('name')
+            label_idx = label_categories.find(label_name)[0]
+            if label_idx is None:
+                label_idx = label_categories.add(label_name)
+
+        for id, asset in anno_dict.get('assets', {}).items():
+            item_id = osp.splitext(asset.get('asset', {}).get('name'))[0]
+            annotations = []
+            for region in asset.get('regions', []):
+                tags = region.get('tags', [])
+                if not tags:
+                    bbox = region.get('boundingBox', {})
+                    if bbox:
+                        annotations.append(Bbox(float(bbox['left']), float(bbox['top']),
+                            float(bbox['width']), float(bbox['height']),
+                            attributes={'id': region.get('id')}))
+
+                for tag in region.get('tags', []):
+                    label_idx = label_categories.find(tag)[0]
+                    if label_idx is None:
+                        label_idx = label_categories.add(tag)
+
+                    bbox = region.get('boundingBox', {})
+                    if bbox:
+                        annotations.append(Bbox(float(bbox['left']), float(bbox['top']),
+                            float(bbox['width']), float(bbox['height']), label=label_idx,
+                            attributes={'id': region.get('id')}))
+
+            items[item_id] = DatasetItem(id=item_id, subset=self._subset, attributes={'id': id},
+                image=Image(path=osp.join(osp.dirname(path), asset.get('asset', {}).get('path'))),
+                annotations=annotations)
+
+        return items
+
+class VottJsonImporter(Importer):
+    @classmethod
+    def find_sources(cls, path):
+        return cls._find_sources_recursive(path, '.json', 'vott_json')
+
+    @classmethod
+    def detect(cls, context: FormatDetectionContext) -> None:
+        context.require_file('*' + VottJsonPath.ANNO_FILE_SUFFIX)


Test patch:
diff --git a/tests/assets/vott_csv_dataset/dataset/img0001.jpg b/tests/assets/vott_csv_dataset/dataset/img0001.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_csv_dataset/dataset/img0001.jpg differ
diff --git a/tests/assets/vott_csv_dataset/dataset/img0002.jpg b/tests/assets/vott_csv_dataset/dataset/img0002.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_csv_dataset/dataset/img0002.jpg differ
diff --git a/tests/assets/vott_csv_dataset/dataset/img0003.jpg b/tests/assets/vott_csv_dataset/dataset/img0003.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_csv_dataset/dataset/img0003.jpg differ
diff --git a/tests/assets/vott_csv_dataset/dataset/img0004.jpg b/tests/assets/vott_csv_dataset/dataset/img0004.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_csv_dataset/dataset/img0004.jpg differ
diff --git a/tests/assets/vott_csv_dataset/dataset/test-export.csv b/tests/assets/vott_csv_dataset/dataset/test-export.csv
new file mode 100644
--- /dev/null
+++ b/tests/assets/vott_csv_dataset/dataset/test-export.csv
@@ -0,0 +1,3 @@
+"image","xmin","ymin","xmax","ymax","label"
+"img0001.jpg",10,5,20,7,"helmet"
+"img0002.jpg",11.5,12,21.7,32.5,"person"
diff --git a/tests/assets/vott_csv_dataset/dataset/train-export.csv b/tests/assets/vott_csv_dataset/dataset/train-export.csv
new file mode 100644
--- /dev/null
+++ b/tests/assets/vott_csv_dataset/dataset/train-export.csv
@@ -0,0 +1,4 @@
+"image","xmin","ymin","xmax","ymax","label"
+"img0003.jpg",6.7,10.3,10,15,"helmet"
+"img0004.jpg",1,2,2,4,"helmet"
+"img0003.jpg",13.7,20.2,45.6,63.6,"person"
diff --git a/tests/assets/vott_csv_dataset/dataset_with_meta_file/dataset_meta.json b/tests/assets/vott_csv_dataset/dataset_with_meta_file/dataset_meta.json
new file mode 100644
--- /dev/null
+++ b/tests/assets/vott_csv_dataset/dataset_with_meta_file/dataset_meta.json
@@ -0,0 +1,3 @@
+{
+"labels": ["helmet", "person"]
+}
diff --git a/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0001.jpg b/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0001.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0001.jpg differ
diff --git a/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0002.jpg b/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0002.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0002.jpg differ
diff --git a/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0003.jpg b/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0003.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0003.jpg differ
diff --git a/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0004.jpg b/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0004.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_csv_dataset/dataset_with_meta_file/img0004.jpg differ
diff --git a/tests/assets/vott_csv_dataset/dataset_with_meta_file/test-export.csv b/tests/assets/vott_csv_dataset/dataset_with_meta_file/test-export.csv
new file mode 100644
--- /dev/null
+++ b/tests/assets/vott_csv_dataset/dataset_with_meta_file/test-export.csv
@@ -0,0 +1,3 @@
+"image","xmin","ymin","xmax","ymax","label"
+"img0001.jpg",10,5,20,7,"helmet"
+"img0002.jpg",11.5,12,21.7,32.5,"person"
diff --git a/tests/assets/vott_json_dataset/dataset/img0001.jpg b/tests/assets/vott_json_dataset/dataset/img0001.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_json_dataset/dataset/img0001.jpg differ
diff --git a/tests/assets/vott_json_dataset/dataset/img0002.jpg b/tests/assets/vott_json_dataset/dataset/img0002.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_json_dataset/dataset/img0002.jpg differ
diff --git a/tests/assets/vott_json_dataset/dataset/img0003.jpg b/tests/assets/vott_json_dataset/dataset/img0003.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_json_dataset/dataset/img0003.jpg differ
diff --git a/tests/assets/vott_json_dataset/dataset/train-export.json b/tests/assets/vott_json_dataset/dataset/train-export.json
new file mode 100644
--- /dev/null
+++ b/tests/assets/vott_json_dataset/dataset/train-export.json
@@ -0,0 +1,206 @@
+{
+    "name": "test",
+    "videoSettings": {
+        "frameExtractionRate": 15
+    },
+    "tags": [
+        {
+            "name": "animal",
+            "color": "#5db300"
+        },
+        {
+            "name": "dog",
+            "color": "#e81123"
+        },
+        {
+            "name": "person",
+            "color": "#6917aa"
+        }
+    ],
+    "useSecurityToken": true,
+    "securityToken": "test Token",
+    "id": "0OPxbBiQE",
+    "activeLearningSettings": {
+        "autoDetect": false,
+        "predictTag": true,
+        "modelPathType": "coco"
+    },
+    "version": "2.2.0",
+    "lastVisitedAssetId": "0d3de147fea94f1797f9d012acad9666",
+    "assets": {
+        "0d3de147f": {
+            "asset": {
+                "format": "jpg",
+                "id": "0d3de147f",
+                "name": "img0001.jpg",
+                "path": "img0001.jpg",
+                "size": {
+                    "width": 5,
+                    "height": 5
+                },
+                "state": 1,
+                "type": 1
+            },
+            "regions": [
+                {
+                    "id": "BsO3zj9bn",
+                    "type": "RECTANGLE",
+                    "tags": [
+                        "animal"
+                    ],
+                    "boundingBox": {
+                        "height": 2,
+                        "width": 10,
+                        "left": 5,
+                        "top": 10
+                    },
+                    "points": [
+                        {
+                            "x": 5,
+                            "y": 10
+                        },
+                        {
+                            "x": 15,
+                            "y": 10
+                        },
+                        {
+                            "x": 15,
+                            "y": 12
+                        },
+                        {
+                            "x": 5,
+                            "y": 12
+                        }
+                    ]
+                }
+            ],
+            "version": "2.2.0"
+        },
+        "b482849bc": {
+            "asset": {
+                "format": "jpeg",
+                "id": "b482849bc",
+                "name": "img0002.jpg",
+                "path": "img0002.jpg",
+                "size": {
+                    "width": 5,
+                    "height": 5
+                },
+                "state": 2,
+                "type": 1
+            },
+            "regions": [
+                {
+                    "id": "mosw0b97K",
+                    "type": "RECTANGLE",
+                    "tags": [
+                        "animal", "dog"
+                    ],
+                    "boundingBox": {
+                        "height": 20.5,
+                        "width": 10.2,
+                        "left": 11.5,
+                        "top": 12
+                    },
+                    "points": [
+                        {
+                            "x": 11.5,
+                            "y": 12
+                        },
+                        {
+                            "x": 21.7,
+                            "y": 12
+                        },
+                        {
+                            "x": 21.7,
+                            "y": 32.5
+                        },
+                        {
+                            "x": 11.5,
+                            "y": 32.5
+                        }
+                    ]
+                }
+            ],
+            "version": "2.2.0"
+        },
+        "50fef05a8": {
+            "asset": {
+                "format": "jpeg",
+                "id": "50fef05a8",
+                "name": "img0003.jpg",
+                "path": "img0003.jpg",
+                "size": {
+                    "width": 5,
+                    "height": 5
+                },
+                "state": 2,
+                "type": 1
+            },
+            "regions": [
+                {
+                    "id": "35t9mf-Zr",
+                    "type": "RECTANGLE",
+                    "tags": [
+                    ],
+                    "boundingBox": {
+                        "height": 4.7,
+                        "width": 3.3,
+                        "left": 6.7,
+                        "top": 10.3
+                    },
+                    "points": [
+                        {
+                            "x": 6.7,
+                            "y": 10.3
+                        },
+                        {
+                            "x": 10,
+                            "y": 10.3
+                        },
+                        {
+                            "x": 10,
+                            "y": 15
+                        },
+                        {
+                            "x": 6.7,
+                            "y": 15
+                        }
+                    ]
+                },
+                {
+                    "id": "sO4m1DtTZ",
+                    "type": "RECTANGLE",
+                    "tags": [
+                        "person"
+                    ],
+                    "boundingBox": {
+                        "height": 43.4,
+                        "width": 31.9,
+                        "left": 13.7,
+                        "top": 20.2
+                    },
+                    "points": [
+                        {
+                            "x": 13.7,
+                            "y": 20.2
+                        },
+                        {
+                            "x": 45.6,
+                            "y": 20.2
+                        },
+                        {
+                            "x": 45.6,
+                            "y": 63.6
+                        },
+                        {
+                            "x": 13.7,
+                            "y": 63.6
+                        }
+                    ]
+                }
+            ],
+            "version": "2.2.0"
+        }
+    }
+}
diff --git a/tests/assets/vott_json_dataset/dataset_with_meta_file/dataset_meta.json b/tests/assets/vott_json_dataset/dataset_with_meta_file/dataset_meta.json
new file mode 100644
--- /dev/null
+++ b/tests/assets/vott_json_dataset/dataset_with_meta_file/dataset_meta.json
@@ -0,0 +1,3 @@
+{
+"labels": ["animal", "dog", "person"]
+}
diff --git a/tests/assets/vott_json_dataset/dataset_with_meta_file/img0001.jpg b/tests/assets/vott_json_dataset/dataset_with_meta_file/img0001.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_json_dataset/dataset_with_meta_file/img0001.jpg differ
diff --git a/tests/assets/vott_json_dataset/dataset_with_meta_file/img0002.jpg b/tests/assets/vott_json_dataset/dataset_with_meta_file/img0002.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_json_dataset/dataset_with_meta_file/img0002.jpg differ
diff --git a/tests/assets/vott_json_dataset/dataset_with_meta_file/img0003.jpg b/tests/assets/vott_json_dataset/dataset_with_meta_file/img0003.jpg
new file mode 100644
Binary files /dev/null and b/tests/assets/vott_json_dataset/dataset_with_meta_file/img0003.jpg differ
diff --git a/tests/assets/vott_json_dataset/dataset_with_meta_file/train-export.json b/tests/assets/vott_json_dataset/dataset_with_meta_file/train-export.json
new file mode 100644
--- /dev/null
+++ b/tests/assets/vott_json_dataset/dataset_with_meta_file/train-export.json
@@ -0,0 +1,206 @@
+{
+    "name": "test",
+    "videoSettings": {
+        "frameExtractionRate": 15
+    },
+    "tags": [
+        {
+            "name": "animal",
+            "color": "#5db300"
+        },
+        {
+            "name": "dog",
+            "color": "#e81123"
+        },
+        {
+            "name": "person",
+            "color": "#6917aa"
+        }
+    ],
+    "useSecurityToken": true,
+    "securityToken": "test Token",
+    "id": "0OPxbBiQE",
+    "activeLearningSettings": {
+        "autoDetect": false,
+        "predictTag": true,
+        "modelPathType": "coco"
+    },
+    "version": "2.2.0",
+    "lastVisitedAssetId": "0d3de147fea94f1797f9d012acad9666",
+    "assets": {
+        "0d3de147f": {
+            "asset": {
+                "format": "jpg",
+                "id": "0d3de147f",
+                "name": "img0001.jpg",
+                "path": "img0001.jpg",
+                "size": {
+                    "width": 5,
+                    "height": 5
+                },
+                "state": 1,
+                "type": 1
+            },
+            "regions": [
+                {
+                    "id": "BsO3zj9bn",
+                    "type": "RECTANGLE",
+                    "tags": [
+                        "animal"
+                    ],
+                    "boundingBox": {
+                        "height": 2,
+                        "width": 10,
+                        "left": 5,
+                        "top": 10
+                    },
+                    "points": [
+                        {
+                            "x": 5,
+                            "y": 10
+                        },
+                        {
+                            "x": 15,
+                            "y": 10
+                        },
+                        {
+                            "x": 15,
+                            "y": 12
+                        },
+                        {
+                            "x": 5,
+                            "y": 12
+                        }
+                    ]
+                }
+            ],
+            "version": "2.2.0"
+        },
+        "b482849bc": {
+            "asset": {
+                "format": "jpeg",
+                "id": "b482849bc",
+                "name": "img0002.jpg",
+                "path": "img0002.jpg",
+                "size": {
+                    "width": 5,
+                    "height": 5
+                },
+                "state": 2,
+                "type": 1
+            },
+            "regions": [
+                {
+                    "id": "mosw0b97K",
+                    "type": "RECTANGLE",
+                    "tags": [
+                        "dog"
+                    ],
+                    "boundingBox": {
+                        "height": 20.5,
+                        "width": 10.2,
+                        "left": 11.5,
+                        "top": 12
+                    },
+                    "points": [
+                        {
+                            "x": 11.5,
+                            "y": 12
+                        },
+                        {
+                            "x": 21.7,
+                            "y": 12
+                        },
+                        {
+                            "x": 21.7,
+                            "y": 32.5
+                        },
+                        {
+                            "x": 11.5,
+                            "y": 32.5
+                        }
+                    ]
+                }
+            ],
+            "version": "2.2.0"
+        },
+        "50fef05a8": {
+            "asset": {
+                "format": "jpeg",
+                "id": "50fef05a8",
+                "name": "img0003.jpg",
+                "path": "img0003.jpg",
+                "size": {
+                    "width": 5,
+                    "height": 5
+                },
+                "state": 2,
+                "type": 1
+            },
+            "regions": [
+                {
+                    "id": "35t9mf-Zr",
+                    "type": "RECTANGLE",
+                    "tags": [
+                    ],
+                    "boundingBox": {
+                        "height": 4.7,
+                        "width": 3.3,
+                        "left": 6.7,
+                        "top": 10.3
+                    },
+                    "points": [
+                        {
+                            "x": 6.7,
+                            "y": 10.3
+                        },
+                        {
+                            "x": 10,
+                            "y": 10.3
+                        },
+                        {
+                            "x": 10,
+                            "y": 15
+                        },
+                        {
+                            "x": 6.7,
+                            "y": 15
+                        }
+                    ]
+                },
+                {
+                    "id": "sO4m1DtTZ",
+                    "type": "RECTANGLE",
+                    "tags": [
+                        "person"
+                    ],
+                    "boundingBox": {
+                        "height": 43.4,
+                        "width": 31.9,
+                        "left": 13.7,
+                        "top": 20.2
+                    },
+                    "points": [
+                        {
+                            "x": 13.7,
+                            "y": 20.2
+                        },
+                        {
+                            "x": 45.6,
+                            "y": 20.2
+                        },
+                        {
+                            "x": 45.6,
+                            "y": 63.6
+                        },
+                        {
+                            "x": 13.7,
+                            "y": 63.6
+                        }
+                    ]
+                }
+            ],
+            "version": "2.2.0"
+        }
+    }
+}
diff --git a/tests/test_vott_csv_format.py b/tests/test_vott_csv_format.py
new file mode 100644
--- /dev/null
+++ b/tests/test_vott_csv_format.py
@@ -0,0 +1,86 @@
+from unittest import TestCase
+import os.path as osp
+
+import numpy as np
+
+from datumaro.components.annotation import Bbox
+from datumaro.components.dataset import Dataset
+from datumaro.components.environment import Environment
+from datumaro.components.extractor import DatasetItem
+from datumaro.plugins.vott_csv_format import VottCsvImporter
+from datumaro.util.test_utils import compare_datasets
+
+from .requirements import Requirements, mark_requirement
+
+DUMMY_DATASET_DIR = osp.join(osp.dirname(__file__), 'assets',
+    'vott_csv_dataset', 'dataset')
+DUMMY_DATASET_DIR_WITH_META_FILE = osp.join(osp.dirname(__file__),
+    'assets', 'vott_csv_dataset', 'dataset_with_meta_file')
+
+class VottCsvImporterTest(TestCase):
+    @mark_requirement(Requirements.DATUM_475)
+    def test_can_import(self):
+        expected_dataset = Dataset.from_iterable([
+            DatasetItem(id='img0001', subset='test',
+                image=np.ones((5, 5, 3)),
+                annotations=[
+                    Bbox(10, 5, 10, 2, label=0)
+                ]
+            ),
+            DatasetItem(id='img0002', subset='test',
+                image=np.ones((5, 5, 3)),
+                annotations=[
+                    Bbox(11.5, 12, 10.2, 20.5, label=1),
+                ]
+            ),
+            DatasetItem(id='img0003', subset='train',
+                image=np.ones((5, 5, 3)),
+                annotations=[
+                    Bbox(6.7, 10.3, 3.3, 4.7, label=0),
+                    Bbox(13.7, 20.2, 31.9, 43.4, label=1),
+                ]
+            ),
+            DatasetItem(id='img0004', subset='train',
+                image=np.ones((5, 5, 3)),
+                annotations=[
+                    Bbox(1, 2, 1, 2, label=0),
+                ]
+            )
+        ], categories=['helmet', 'person'])
+
+        dataset = Dataset.import_from(DUMMY_DATASET_DIR, 'vott_csv')
+
+        compare_datasets(self, expected_dataset, dataset, require_images=True)
+
+    @mark_requirement(Requirements.DATUM_475)
+    def test_can_import_with_meta_file(self):
+        expected_dataset = Dataset.from_iterable([
+            DatasetItem(id='img0001', subset='test',
+                image=np.ones((5, 5, 3)),
+                annotations=[
+                    Bbox(10, 5, 10, 2, label=0)
+                ]
+            ),
+            DatasetItem(id='img0002', subset='test',
+                image=np.ones((5, 5, 3)),
+                annotations=[
+                    Bbox(11.5, 12, 10.2, 20.5, label=1),
+                ]
+            )
+        ], categories=['helmet', 'person'])
+
+        dataset = Dataset.import_from(DUMMY_DATASET_DIR_WITH_META_FILE,
+            'vott_csv')
+
+        compare_datasets(self, expected_dataset, dataset, require_images=True)
+
+    @mark_requirement(Requirements.DATUM_475)
+    def test_can_detect(self):
+        detected_formats = Environment().detect_dataset(DUMMY_DATASET_DIR)
+        self.assertEqual([VottCsvImporter.NAME], detected_formats)
+
+    @mark_requirement(Requirements.DATUM_475)
+    def test_can_detect_with_meta_file(self):
+        detected_formats = \
+            Environment().detect_dataset(DUMMY_DATASET_DIR_WITH_META_FILE)
+        self.assertEqual([VottCsvImporter.NAME], detected_formats)
diff --git a/tests/test_vott_json_format.py b/tests/test_vott_json_format.py
new file mode 100644
--- /dev/null
+++ b/tests/test_vott_json_format.py
@@ -0,0 +1,102 @@
+from unittest import TestCase
+import os.path as osp
+
+import numpy as np
+
+from datumaro.components.annotation import Bbox
+from datumaro.components.dataset import Dataset
+from datumaro.components.environment import Environment
+from datumaro.components.extractor import DatasetItem
+from datumaro.plugins.vott_json_format import VottJsonImporter
+from datumaro.util.test_utils import compare_datasets
+
+from .requirements import Requirements, mark_requirement
+
+DUMMY_DATASET_DIR = osp.join(osp.dirname(__file__), 'assets',
+    'vott_json_dataset', 'dataset')
+DUMMY_DATASET_DIR_WITH_META_FILE = osp.join(osp.dirname(__file__),
+    'assets', 'vott_json_dataset', 'dataset_with_meta_file')
+
+class VottJsonImporterTest(TestCase):
+    @mark_requirement(Requirements.DATUM_475)
+    def test_can_import(self):
+        expected_dataset = Dataset.from_iterable([
+            DatasetItem(id='img0001',
+                subset='train', image=np.ones((5, 5, 3)),
+                attributes={'id': '0d3de147f'},
+                annotations=[
+                    Bbox(5, 10, 10, 2, label=0,
+                        attributes={'id': 'BsO3zj9bn'})
+                ]
+            ),
+            DatasetItem(id='img0002',
+                subset='train', image=np.ones((5, 5, 3)),
+                attributes={'id': 'b482849bc'},
+                annotations=[
+                    Bbox(11.5, 12, 10.2, 20.5, label=0,
+                        attributes={'id': 'mosw0b97K'}),
+                    Bbox(11.5, 12, 10.2, 20.5, label=1,
+                        attributes={'id': 'mosw0b97K'})
+                ]
+            ),
+            DatasetItem(id='img0003',
+                subset='train', image=np.ones((5, 5, 3)),
+                attributes={'id': '50fef05a8'},
+                annotations=[
+                    Bbox(6.7, 10.3, 3.3, 4.7,
+                        attributes={'id': '35t9mf-Zr'}),
+                    Bbox(13.7, 20.2, 31.9, 43.4, label=2,
+                        attributes={'id': 'sO4m1DtTZ'})
+                ]
+            )
+        ], categories=['animal', 'dog', 'person'])
+
+        dataset = Dataset.import_from(DUMMY_DATASET_DIR, 'vott_json')
+
+        compare_datasets(self, expected_dataset, dataset, require_images=True)
+
+    @mark_requirement(Requirements.DATUM_475)
+    def test_can_import_with_meta_file(self):
+        expected_dataset = Dataset.from_iterable([
+            DatasetItem(id='img0001',
+                subset='train', image=np.ones((5, 5, 3)),
+                attributes={'id': '0d3de147f'},
+                annotations=[
+                    Bbox(5, 10, 10, 2, label=0,
+                        attributes={'id': 'BsO3zj9bn'})
+                ]
+            ),
+            DatasetItem(id='img0002',
+                subset='train', image=np.ones((5, 5, 3)),
+                attributes={'id': 'b482849bc'},
+                annotations=[
+                    Bbox(11.5, 12, 10.2, 20.5, label=1,
+                        attributes={'id': 'mosw0b97K'})
+                ]
+            ),
+            DatasetItem(id='img0003',
+                subset='train', image=np.ones((5, 5, 3)),
+                attributes={'id': '50fef05a8'},
+                annotations=[
+                    Bbox(6.7, 10.3, 3.3, 4.7,
+                        attributes={'id': '35t9mf-Zr'}),
+                    Bbox(13.7, 20.2, 31.9, 43.4, label=2,
+                        attributes={'id': 'sO4m1DtTZ'})
+                ]
+            )
+        ], categories=['animal', 'dog', 'person'])
+
+        dataset = Dataset.import_from(DUMMY_DATASET_DIR_WITH_META_FILE, 'vott_json')
+
+        compare_datasets(self, expected_dataset, dataset, require_images=True)
+
+    @mark_requirement(Requirements.DATUM_475)
+    def test_can_detect(self):
+        detected_formats = Environment().detect_dataset(DUMMY_DATASET_DIR)
+        self.assertEqual([VottJsonImporter.NAME], detected_formats)
+
+    @mark_requirement(Requirements.DATUM_475)
+    def test_can_detect_with_meta_file(self):
+        detected_formats = \
+            Environment().detect_dataset(DUMMY_DATASET_DIR_WITH_META_FILE)
+        self.assertEqual([VottJsonImporter.NAME], detected_formats)