diff --git a/.gitignore b/.gitignore index 98d2bb2..55e0d53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -data/* venv/ .idea/ -*.pyc \ No newline at end of file +*.pyc +.vscode/* \ No newline at end of file diff --git a/README.md b/README.md index 0a9d651..427dada 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -## Cityscapes to CoCo Conversion Tool +## Cityscapes to COCO Conversion Tool ![](assets/preview.png) -This script allows to convert the [Cityscapes Dataset](https://www.cityscapes-dataset.com/) to Mircosoft's [CoCo Format](http://cocodataset.org/). The code heavily relies on Facebook's [Detection Repo](https://github.com/facebookresearch/Detectron/blob/master/tools/convert_cityscapes_to_coco.py) and [Cityscapes Scripts](https://github.com/mcordts/cityscapesScripts). +Forked from https://github.com/TillBeemelmanns/cityscapes-to-coco-conversion + +This script allows to convert the [Cityscapes Dataset](https://www.cityscapes-dataset.com/) to Mircosoft's [COCO Format](http://cocodataset.org/). The code heavily relies on Facebook's [Detection Repo](https://github.com/facebookresearch/Detectron/blob/master/tools/convert_cityscapes_to_coco.py) and [Cityscapes Scripts](https://github.com/mcordts/cityscapesScripts). The converted annotations can be easily used for [Mask-RCNN](https://github.com/matterport/Mask_RCNN) or other deep learning projects. @@ -20,6 +22,7 @@ data/ ├── test ├── train └── val +utils/ main.py inspect_coco.py README.md @@ -27,21 +30,69 @@ requirements.txt ``` ## Installation -``` +```shell pip install -r requirements.txt ``` - ## Run To run the conversion execute the following -``` +```shell python main.py --dataset cityscapes --datadir data/cityscapes --outdir data/cityscapes/annotations ``` -In order to run the visualization of the CoCo dataset you may run +Takes about 12 minutes to execute. + +The script will create the files + +- ```instancesonly_filtered_gtFine_train.json``` +- ```instancesonly_filtered_gtFine_val.json``` + +in the directory ```annotations``` for the ```train``` and ```val``` split which contain the Coco annotations. + +The variable category_instancesonly defines which classes should be considered in the conversion process. By default has this value: + +```python +category_instancesonly = [ + 'person', + 'rider', + 'car', + 'truck', + 'bus', + 'train', + 'motorcycle', + 'bicycle', +] +``` + +which in COCO format (in .yaml file format) is + +```yaml +NUM_CLASSES: 9 +CLASSES: [ + { 'supercategory': 'none', 'id': 0, 'name': 'background' }, + { 'supercategory': 'none', 'id': 1, 'name': 'person' }, + { 'supercategory': 'none', 'id': 2, 'name': 'rider' }, + { 'supercategory': 'none', 'id': 3, 'name': 'car' }, + { 'supercategory': 'none', 'id': 4, 'name': 'bicycle' }, + { 'supercategory': 'none', 'id': 5, 'name': 'motorcycle' }, + { 'supercategory': 'none', 'id': 6, 'name': 'bus' }, + { 'supercategory': 'none', 'id': 7, 'name': 'truck' }, + { 'supercategory': 'none', 'id': 8, 'name': 'train' }, +] ``` + +It is not possible to enable more classes as there is no instance annotation + +Sometimes the segmentation annotations are so small that no reasonable big enough object could be created. In this case the, the object will be skipped and the following message is printed: + +``` +Warning: invalid contours. +``` + +In order to run the visualization of the COCO dataset you may run +```shell python inspect_coco.py --coco_dir data/cityscapes ``` ## Output -![vis1](assets/plot1.png "Cityscapes in CoCo format") ![vis2](assets/plot2.png "Cityscapes in CoCo format") \ No newline at end of file +![vis1](assets/plot1.png "Cityscapes in COCO format") ![vis2](assets/plot2.png "Cityscapes in COCO format") \ No newline at end of file diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/inspect_coco.py b/inspect_coco.py index 6b1136a..40aedc8 100644 --- a/inspect_coco.py +++ b/inspect_coco.py @@ -5,6 +5,7 @@ import utils from utils import visualize from utils.utils import CocoDataset +import numpy as np def main(coco_dir, num_plot_examples): @@ -20,14 +21,14 @@ def main(coco_dir, num_plot_examples): # plot masks for each class for _ in range(num_plot_examples): - random_image_id = random.choice(dataset.image_ids) + random_image_id = np.random.choice(dataset.image_ids) image = dataset.load_image(random_image_id) mask, class_ids = dataset.load_mask(random_image_id) visualize.display_top_masks(image, mask, class_ids, dataset.class_names) # Plot display instances for _ in range(num_plot_examples): - random_image_id = random.choice(dataset.image_ids) + random_image_id = np.random.choice(dataset.image_ids) image = dataset.load_image(random_image_id) mask, class_ids = dataset.load_mask(random_image_id) bbox = utils.utils.extract_bboxes(mask) diff --git a/main.py b/main.py index ba78617..8f165b4 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,8 @@ from __future__ import unicode_literals import sys +from typing import OrderedDict +from pathlib import Path # Image processing # Check if PIL is actually Pillow as expected @@ -132,19 +134,25 @@ def convert_cityscapes_instance_only(data_dir, out_dir): img_id = 0 ann_id = 0 cat_id = 1 - category_dict = {} + category_dict = OrderedDict() category_instancesonly = [ 'person', 'rider', 'car', - 'truck', + 'bicycle', + 'motorcycle', 'bus', + 'truck', 'train', - 'motorcycle', - 'bicycle', ] + # It is not possible to enable more classes as there is no instance annotation of that classes + + # Fill the category dict in an ordered manner + for i, cat in enumerate(category_instancesonly): + category_dict[cat] = i + 1 # +1 to start from 1 (category 0 is for BG in Faster RCNN) + for data_set, ann_dir in zip(sets, ann_dirs): print('Starting %s' % data_set) ann_dict = {} @@ -165,11 +173,13 @@ def convert_cityscapes_instance_only(data_dir, out_dir): img_id += 1 image['width'] = json_ann['imgWidth'] image['height'] = json_ann['imgHeight'] - image['file_name'] = os.path.join("leftImg8bit", - data_set.split("/")[-1], - filename.split('_')[0], - filename.replace("_gtFine_polygons.json", '_leftImg8bit.png')) - image['seg_file_name'] = filename.replace("_polygons.json", "_instanceIds.png") + image['file_name'] = Path( + os.path.join("leftImg8bit", + data_set.split("/")[-1], + filename.split('_')[0], + filename.replace("_gtFine_polygons.json", '_leftImg8bit.png')) + ).as_posix() + image['seg_file_name'] = Path(filename.replace("_polygons.json", "_instanceIds.png")).as_posix() images.append(image) fullname = os.path.join(root, image['seg_file_name']) @@ -185,6 +195,8 @@ def convert_cityscapes_instance_only(data_dir, out_dir): continue # skip non-instance categories len_p = [len(p) for p in obj['contours']] + if object_cls == 'traffic sign': + print("New label found") if min(len_p) <= 4: print('Warning: invalid contours.') continue # skip non-instance categories @@ -195,9 +207,10 @@ def convert_cityscapes_instance_only(data_dir, out_dir): ann['image_id'] = image['id'] ann['segmentation'] = obj['contours'] - if object_cls not in category_dict: - category_dict[object_cls] = cat_id - cat_id += 1 + # if object_cls not in category_dict: + # category_dict[object_cls] = cat_id + # cat_id += 1 + ann['category_id'] = category_dict[object_cls] ann['iscrowd'] = 0 ann['area'] = obj['pixelCount'] diff --git a/requirements.txt b/requirements.txt index 6ff6564..58a0069 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -numpy -h5py -scipy -Pillow -opencv-python -pycocotools -scikit-image +numpy==1.24.2 +h5py==3.8.0 +scipy==1.10.1 +Pillow==9.4.0 +opencv-python==4.7.0.72 +pycocotools==2.0.6 +scikit-image==0.20.0 \ No newline at end of file diff --git a/utils/utils.py b/utils/utils.py index 31805c8..4844fdc 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -345,7 +345,7 @@ def minimize_mask(bbox, mask, mini_shape): raise Exception("Invalid bounding box with area of zero") # Resize with bilinear interpolation m = resize(m, mini_shape) - mini_mask[:, :, i] = np.around(m).astype(np.bool) + mini_mask[:, :, i] = np.around(m).astype(bool) return mini_mask @@ -363,7 +363,7 @@ def expand_mask(bbox, mini_mask, image_shape): w = x2 - x1 # Resize with bilinear interpolation m = resize(m, (h, w)) - mask[y1:y2, x1:x2, i] = np.around(m).astype(np.bool) + mask[y1:y2, x1:x2, i] = np.around(m).astype(bool) return mask @@ -383,10 +383,10 @@ def unmold_mask(mask, bbox, image_shape): threshold = 0.5 y1, x1, y2, x2 = bbox mask = resize(mask, (y2 - y1, x2 - x1)) - mask = np.where(mask >= threshold, 1, 0).astype(np.bool) + mask = np.where(mask >= threshold, 1, 0).astype(bool) # Put the mask in the right location. - full_mask = np.zeros(image_shape[:2], dtype=np.bool) + full_mask = np.zeros(image_shape[:2], dtype=bool) full_mask[y1:y2, x1:x2] = mask return full_mask @@ -487,7 +487,7 @@ def load_mask(self, image_id): # Pack instance masks into an array if class_ids: - mask = np.stack(instance_masks, axis=2).astype(np.bool) + mask = np.stack(instance_masks, axis=2).astype(bool) class_ids = np.array(class_ids, dtype=np.int32) return mask, class_ids else: