From 876df38dd5971c6a8887ec20ae96e3e58b7feba3 Mon Sep 17 00:00:00 2001 From: 110414 Date: Thu, 16 Mar 2023 11:44:49 +0100 Subject: [PATCH 1/5] Improved the way to create the dictionary with the categories, the instructions on README and solved problem with inspect --- .gitignore | 4 ++-- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++------ data/.gitignore | 2 ++ inspect_coco.py | 5 ++-- main.py | 25 ++++++++++++++------ utils/utils.py | 10 ++++---- 6 files changed, 86 insertions(+), 23 deletions(-) create mode 100644 data/.gitignore 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..ee1a2f2 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,67 @@ 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' }, +] +``` + +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..57bfea0 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import sys +from typing import OrderedDict # Image processing # Check if PIL is actually Pillow as expected @@ -132,19 +133,28 @@ 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', ] + # To enable other possible categories like "traffic sign", "traffic light", "pole", + # the contours must be extracted from _gtFine_labelIds.png images, where the labels are + # encoded in the colors. I have not tried to do it with findContours from cv2, but it should + # work, with some little modifications to the rest of the code + + # 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 = {} @@ -195,9 +205,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/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: From 0ec52dda9ead55da69bffa503d7ab2109e05d6c8 Mon Sep 17 00:00:00 2001 From: 110414 Date: Fri, 17 Mar 2023 13:01:36 +0100 Subject: [PATCH 2/5] Update python requirements --- requirements.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 From aba280c4ceb89b2e13c1395ea93400888e3af208 Mon Sep 17 00:00:00 2001 From: 110414 Date: Fri, 17 Mar 2023 13:02:25 +0100 Subject: [PATCH 3/5] Add comment about new classes --- main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 57bfea0..7fcaad0 100644 --- a/main.py +++ b/main.py @@ -146,10 +146,7 @@ def convert_cityscapes_instance_only(data_dir, out_dir): 'train', ] - # To enable other possible categories like "traffic sign", "traffic light", "pole", - # the contours must be extracted from _gtFine_labelIds.png images, where the labels are - # encoded in the colors. I have not tried to do it with findContours from cv2, but it should - # work, with some little modifications to the rest of the code + # 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): @@ -195,6 +192,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 From 0f0681523d0e8af1715b811dced2c7833c31e0ac Mon Sep 17 00:00:00 2001 From: 110414 Date: Fri, 17 Mar 2023 13:02:35 +0100 Subject: [PATCH 4/5] Update README instructions --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ee1a2f2..427dada 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ CLASSES: [ ] ``` +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: ``` From 7bc62a51cd00d7959308917d782e0e752b427ee5 Mon Sep 17 00:00:00 2001 From: 110414 Date: Tue, 21 Mar 2023 17:44:51 +0100 Subject: [PATCH 5/5] Use pathlib to generate posix paths --- main.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 7fcaad0..8f165b4 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ import sys from typing import OrderedDict +from pathlib import Path # Image processing # Check if PIL is actually Pillow as expected @@ -172,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'])