├── cityscapesScripts ├── cityscapesscripts │ ├── __init__.py │ ├── annotation │ │ ├── __init__.py │ │ └── icons │ │ │ ├── back.png │ │ │ ├── exit.png │ │ │ ├── next.png │ │ │ ├── open.png │ │ │ ├── play.png │ │ │ ├── plus.png │ │ │ ├── save.png │ │ │ ├── undo.png │ │ │ ├── zoom.png │ │ │ ├── help19.png │ │ │ ├── layerup.png │ │ │ ├── minus.png │ │ │ ├── modify.png │ │ │ ├── shuffle.png │ │ │ ├── checked6.png │ │ │ ├── filepath.png │ │ │ ├── highlight.png │ │ │ ├── layerdown.png │ │ │ ├── newobject.png │ │ │ ├── checked6_red.png │ │ │ ├── clearpolygon.png │ │ │ ├── deleteobject.png │ │ │ ├── screenshot.png │ │ │ └── screenshotToggle.png │ ├── evaluation │ │ ├── __init__.py │ │ ├── addToConfusionMatrix_impl.c │ │ ├── coco.patch │ │ ├── instance.py │ │ ├── instances2dict.py │ │ ├── addToConfusionMatrix.pyx │ │ ├── instances2dict_with_polygons.py │ │ ├── evalPanopticSemanticLabeling.py │ │ ├── evalPixelLevelSemanticLabeling.py │ │ └── evalInstanceLevelSemanticLabeling.py │ ├── helpers │ │ ├── __init__.py │ │ ├── labels_cityPersons.py │ │ ├── csHelpers.py │ │ ├── annotation.py │ │ └── labels.py │ ├── viewer │ │ ├── __init__.py │ │ └── icons │ │ │ ├── back.png │ │ │ ├── disp.png │ │ │ ├── exit.png │ │ │ ├── help19.png │ │ │ ├── minus.png │ │ │ ├── next.png │ │ │ ├── open.png │ │ │ ├── play.png │ │ │ ├── plus.png │ │ │ ├── zoom.png │ │ │ ├── filepath.png │ │ │ └── shuffle.png │ └── preparation │ │ ├── __init__.py │ │ ├── createTrainIdLabelImgs.py │ │ ├── createTrainIdInstanceImgs.py │ │ ├── json2labelImg.py │ │ ├── createPanopticImgs.py │ │ └── json2instanceImg.py ├── setup.cfg ├── docs │ └── csCalibration.pdf ├── .gitignore ├── license.txt ├── setup.py └── README.md ├── vis_coco.py ├── readme.md └── convert_cityscapes_to_coco.py /cityscapesScripts/cityscapesscripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/evaluation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/preparation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesScripts/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /cityscapesScripts/docs/csCalibration.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/docs/csCalibration.pdf -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/back.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/disp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/disp.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/exit.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/help19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/help19.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/minus.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/next.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/open.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/play.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/plus.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/zoom.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/back.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/exit.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/next.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/open.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/play.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/plus.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/save.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/undo.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/zoom.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/filepath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/filepath.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/viewer/icons/shuffle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/viewer/icons/shuffle.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/help19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/help19.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/layerup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/layerup.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/minus.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/modify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/modify.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/shuffle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/shuffle.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/checked6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/checked6.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/filepath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/filepath.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/highlight.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/layerdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/layerdown.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/newobject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/newobject.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/checked6_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/checked6_red.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/clearpolygon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/clearpolygon.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/deleteobject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/deleteobject.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/screenshot.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/annotation/icons/screenshotToggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasjinreal/cityscapestococo/HEAD/cityscapesScripts/cityscapesscripts/annotation/icons/screenshotToggle.png -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/evaluation/addToConfusionMatrix_impl.c: -------------------------------------------------------------------------------- 1 | // cython methods to speed-up evaluation 2 | 3 | void addToConfusionMatrix( const unsigned char* f_prediction_p , 4 | const unsigned char* f_groundTruth_p , 5 | const unsigned int f_width_i , 6 | const unsigned int f_height_i , 7 | unsigned long long* f_confMatrix_p , 8 | const unsigned int f_confMatDim_i ) 9 | { 10 | const unsigned int size_ui = f_height_i * f_width_i; 11 | for (unsigned int i = 0; i < size_ui; ++i) 12 | { 13 | const unsigned char predPx = f_prediction_p [i]; 14 | const unsigned char gtPx = f_groundTruth_p[i]; 15 | f_confMatrix_p[f_confMatDim_i*gtPx + predPx] += 1u; 16 | } 17 | } -------------------------------------------------------------------------------- /cityscapesScripts/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | addToConfusionMatrix.c 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # JSON files 61 | *.json 62 | 63 | # configuration files 64 | cityscapesLabelTool.conf 65 | build/ 66 | -------------------------------------------------------------------------------- /vis_coco.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | this script will using pycoco API 4 | draw our converted annotation to check 5 | if result is right or not 6 | 7 | """ 8 | from pycocotools.coco import COCO 9 | import os 10 | import sys 11 | import cv2 12 | from pycocotools import mask as maskUtils 13 | import numpy as np 14 | import matplotlib.pyplot as plt 15 | from matplotlib.collections import PatchCollection 16 | from matplotlib.patches import Polygon 17 | import skimage.io as io 18 | 19 | 20 | data_dir = './cn_images_20190827' 21 | ann_f = 'annotation/instances_train2014.json' 22 | if len(sys.argv) > 1: 23 | data_dir = sys.argv[1] 24 | ann_f = sys.argv[2] 25 | 26 | coco = COCO(ann_f) 27 | 28 | cats = coco.loadCats(coco.getCatIds()) 29 | print('cats: {}'.format(cats)) 30 | 31 | img_ids = coco.getImgIds() 32 | print('img_ids: {}'.format(img_ids)) 33 | 34 | 35 | for i in range(9): 36 | img = coco.loadImgs(img_ids[i]) 37 | print('checking img: {}, id: {}'.format(img, img_ids[i])) 38 | img_f = os.path.join(data_dir, img[0]['file_name']) 39 | 40 | # draw instances 41 | anno_ids = coco.getAnnIds(imgIds=img[0]['id']) 42 | annos = coco.loadAnns(anno_ids) 43 | 44 | I = io.imread(img_f) 45 | plt.imshow(I) 46 | plt.axis('off') 47 | 48 | coco.showAnns(annos) 49 | plt.show() 50 | 51 | 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # cityscapestococo 2 | 3 | This repo contains all code which convert cityscapes to coco. The scripts actually provided inside Detectron and maskrcnn-benchmark were all down. To help others quickly setup their training pipeline on cityscapes for instance segmentation, this repo is really helpful. 4 | 5 | The converted result cityscapes to coco visualized using `vis_coco.py` inside this repo: 6 | 7 | ![](https://s2.ax1x.com/2019/10/23/KYJqzj.png) 8 | 9 | ![](https://s2.ax1x.com/2019/10/23/KYJxe0.png) 10 | 11 | ![](https://s2.ax1x.com/2019/10/23/KYYCYF.png) 12 | 13 | As you can see, all instance classes were token out from cityscapes, which can be conclude as: 14 | 15 | ``` 16 | [ 17 | 'person', 18 | 'rider', 19 | 'car', 20 | 'truck', 21 | 'bus', 22 | 'train', 23 | 'motorcycle', 24 | 'bicycle', 25 | ] 26 | 27 | ``` 28 | 29 | 30 | 31 | ## Usage 32 | 33 | ``` 34 | python3 convert_cityscapes_to_coco.py 35 | ``` 36 | 37 | For visual: 38 | 39 | ``` 40 | python3 vis_coco.py path/to/annn.json path/to/images/ 41 | ``` 42 | 43 | **note**: You have to move all cityscapes images to a single folder without any subfolders just follow the coco structures. 44 | 45 | 46 | 47 | ## Copyright 48 | 49 | All rights belongs to Fagang Jin, codes released under Apache License 50 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/evaluation/coco.patch: -------------------------------------------------------------------------------- 1 | --- instances2dict.py 2018-02-06 05:19:33.009812039 -0800 2 | +++ instances2dict_with_polygons.py 2018-02-06 05:22:15.953031446 -0800 3 | @@ -11,7 +11,9 @@ 4 | sys.path.append( os.path.normpath( os.path.join( os.path.dirname( __file__ ) , '..' , 'helpers' ) ) ) 5 | from csHelpers import * 6 | 7 | -def instances2dict(imageFileList, verbose=False): 8 | +import cv2 9 | + 10 | +def instances2dict_with_polygons(imageFileList, verbose=False): 11 | imgCount = 0 12 | instanceDict = {} 13 | 14 | @@ -35,9 +37,22 @@ 15 | 16 | # Loop through all instance ids in instance image 17 | for instanceId in np.unique(imgNp): 18 | + if instanceId < 1000: 19 | + continue 20 | + 21 | instanceObj = Instance(imgNp, instanceId) 22 | + instanceObj_dict = instanceObj.toDict() 23 | 24 | - instances[id2label[instanceObj.labelID].name].append(instanceObj.toDict()) 25 | + if id2label[instanceObj.labelID].hasInstances: 26 | + mask = (imgNp == instanceId).astype(np.uint8) 27 | + contour, hier = cv2.findContours( 28 | + mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) 29 | + 30 | + polygons = [c.reshape(-1).tolist() for c in contour] 31 | + instanceObj_dict['contours'] = polygons 32 | + 33 | + instances[id2label[instanceObj.labelID].name].append( 34 | + instanceObj_dict) 35 | 36 | imgKey = os.path.abspath(imageFileName) 37 | instanceDict[imgKey] = instances 38 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/evaluation/instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Instance class 4 | # 5 | 6 | class Instance(object): 7 | instID = 0 8 | labelID = 0 9 | pixelCount = 0 10 | medDist = -1 11 | distConf = 0.0 12 | 13 | def __init__(self, imgNp, instID): 14 | if (instID == -1): 15 | return 16 | self.instID = int(instID) 17 | self.labelID = int(self.getLabelID(instID)) 18 | self.pixelCount = int(self.getInstancePixels(imgNp, instID)) 19 | 20 | def getLabelID(self, instID): 21 | if (instID < 1000): 22 | return instID 23 | else: 24 | return int(instID / 1000) 25 | 26 | def getInstancePixels(self, imgNp, instLabel): 27 | return (imgNp == instLabel).sum() 28 | 29 | def toJSON(self): 30 | return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) 31 | 32 | def toDict(self): 33 | buildDict = {} 34 | buildDict["instID"] = self.instID 35 | buildDict["labelID"] = self.labelID 36 | buildDict["pixelCount"] = self.pixelCount 37 | buildDict["medDist"] = self.medDist 38 | buildDict["distConf"] = self.distConf 39 | return buildDict 40 | 41 | def fromJSON(self, data): 42 | self.instID = int(data["instID"]) 43 | self.labelID = int(data["labelID"]) 44 | self.pixelCount = int(data["pixelCount"]) 45 | if ("medDist" in data): 46 | self.medDist = float(data["medDist"]) 47 | self.distConf = float(data["distConf"]) 48 | 49 | def __str__(self): 50 | return "("+str(self.instID)+")" -------------------------------------------------------------------------------- /cityscapesScripts/license.txt: -------------------------------------------------------------------------------- 1 | ---------------------- 2 | The Cityscapes Dataset 3 | ---------------------- 4 | 5 | 6 | License agreement 7 | ----------------- 8 | 9 | This dataset is made freely available to academic and non-academic entities for non-commercial purposes such as academic research, teaching, scientific publications, or personal experimentation. Permission is granted to use the data given that you agree: 10 | 11 | 1. That the dataset comes "AS IS", without express or implied warranty. Although every effort has been made to ensure accuracy, we (Daimler AG, MPI Informatics, TU Darmstadt) do not accept any responsibility for errors or omissions. 12 | 2. That you include a reference to the Cityscapes Dataset in any work that makes use of the dataset. For research papers, cite our preferred publication as listed on our website; for other media cite our preferred publication as listed on our website or link to the Cityscapes website. 13 | 3. That you do not distribute this dataset or modified versions. It is permissible to distribute derivative works in as far as they are abstract representations of this dataset (such as models trained on it or additional annotations that do not directly include any of our data) and do not allow to recover the dataset or something similar in character. 14 | 4. That you may not use the dataset or any derivative work for commercial purposes as, for example, licensing or selling the data, or using the data with a purpose to procure a commercial gain. 15 | 5. That all rights not expressly granted to you are reserved by us (Daimler AG, MPI Informatics, TU Darmstadt). 16 | 17 | 18 | Contact 19 | ------- 20 | 21 | Marius Cordts, Mohamed Omran 22 | www.cityscapes-dataset.net 23 | mail@cityscapes-dataset.net -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/evaluation/instances2dict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Convert instances from png files to a dictionary 4 | # 5 | 6 | from __future__ import print_function, absolute_import, division 7 | import os, sys 8 | 9 | # Cityscapes imports 10 | from cityscapesscripts.evaluation.instance import * 11 | from cityscapesscripts.helpers.csHelpers import * 12 | 13 | def instances2dict(imageFileList, verbose=False): 14 | imgCount = 0 15 | instanceDict = {} 16 | 17 | if not isinstance(imageFileList, list): 18 | imageFileList = [imageFileList] 19 | 20 | if verbose: 21 | print("Processing {} images...".format(len(imageFileList))) 22 | 23 | for imageFileName in imageFileList: 24 | # Load image 25 | img = Image.open(imageFileName) 26 | 27 | # Image as numpy array 28 | imgNp = np.array(img) 29 | 30 | # Initialize label categories 31 | instances = {} 32 | for label in labels: 33 | instances[label.name] = [] 34 | 35 | # Loop through all instance ids in instance image 36 | for instanceId in np.unique(imgNp): 37 | instanceObj = Instance(imgNp, instanceId) 38 | 39 | instances[id2label[instanceObj.labelID].name].append(instanceObj.toDict()) 40 | 41 | imgKey = os.path.abspath(imageFileName) 42 | instanceDict[imgKey] = instances 43 | imgCount += 1 44 | 45 | if verbose: 46 | print("\rImages Processed: {}".format(imgCount), end=' ') 47 | sys.stdout.flush() 48 | 49 | if verbose: 50 | print("") 51 | 52 | return instanceDict 53 | 54 | def main(argv): 55 | fileList = [] 56 | if (len(argv) > 2): 57 | for arg in argv: 58 | if ("png" in arg): 59 | fileList.append(arg) 60 | instances2dict(fileList, True) 61 | 62 | if __name__ == "__main__": 63 | main(sys.argv[1:]) 64 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/evaluation/addToConfusionMatrix.pyx: -------------------------------------------------------------------------------- 1 | # cython methods to speed-up evaluation 2 | 3 | import numpy as np 4 | cimport cython 5 | cimport numpy as np 6 | import ctypes 7 | 8 | np.import_array() 9 | 10 | cdef extern from "addToConfusionMatrix_impl.c": 11 | void addToConfusionMatrix( const unsigned char* f_prediction_p , 12 | const unsigned char* f_groundTruth_p , 13 | const unsigned int f_width_i , 14 | const unsigned int f_height_i , 15 | unsigned long long* f_confMatrix_p , 16 | const unsigned int f_confMatDim_i ) 17 | 18 | 19 | cdef tonumpyarray(unsigned long long* data, unsigned long long size): 20 | if not (data and size >= 0): raise ValueError 21 | return np.PyArray_SimpleNewFromData(2, [size, size], np.NPY_UINT64, data) 22 | 23 | @cython.boundscheck(False) 24 | def cEvaluatePair( np.ndarray[np.uint8_t , ndim=2] predictionArr , 25 | np.ndarray[np.uint8_t , ndim=2] groundTruthArr , 26 | np.ndarray[np.uint64_t, ndim=2] confMatrix , 27 | evalLabels ): 28 | cdef np.ndarray[np.uint8_t , ndim=2, mode="c"] predictionArr_c 29 | cdef np.ndarray[np.uint8_t , ndim=2, mode="c"] groundTruthArr_c 30 | cdef np.ndarray[np.ulonglong_t, ndim=2, mode="c"] confMatrix_c 31 | 32 | predictionArr_c = np.ascontiguousarray(predictionArr , dtype=np.uint8 ) 33 | groundTruthArr_c = np.ascontiguousarray(groundTruthArr, dtype=np.uint8 ) 34 | confMatrix_c = np.ascontiguousarray(confMatrix , dtype=np.ulonglong) 35 | 36 | cdef np.uint32_t height_ui = predictionArr.shape[1] 37 | cdef np.uint32_t width_ui = predictionArr.shape[0] 38 | cdef np.uint32_t confMatDim_ui = confMatrix.shape[0] 39 | 40 | addToConfusionMatrix(&predictionArr_c[0,0], &groundTruthArr_c[0,0], height_ui, width_ui, &confMatrix_c[0,0], confMatDim_ui) 41 | 42 | confMatrix = np.ascontiguousarray(tonumpyarray(&confMatrix_c[0,0], confMatDim_ui)) 43 | 44 | return np.copy(confMatrix) -------------------------------------------------------------------------------- /cityscapesScripts/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Enable cython support for slightly faster eval scripts: 4 | # python -m pip install cython numpy 5 | # CYTHONIZE_EVAL= python setup.py build_ext --inplace 6 | # 7 | # For MacOS X you may have to export the numpy headers in CFLAGS 8 | # export CFLAGS="-I /usr/local/lib/python3.6/site-packages/numpy/core/include $CFLAGS" 9 | 10 | import os 11 | from setuptools import setup, find_packages 12 | 13 | include_dirs = [] 14 | ext_modules = [] 15 | if 'CYTHONIZE_EVAL' in os.environ: 16 | from Cython.Build import cythonize 17 | import numpy as np 18 | include_dirs = [np.get_include()] 19 | 20 | os.environ["CC"] = "g++" 21 | os.environ["CXX"] = "g++" 22 | 23 | pyxFile = os.path.join("cityscapesscripts", "evaluation", "addToConfusionMatrix.pyx") 24 | ext_modules = cythonize(pyxFile) 25 | 26 | with open("README.md") as f: 27 | readme = f.read() 28 | 29 | config = { 30 | 'name': 'cityscapesScripts', 31 | 'description': 'Scripts for the Cityscapes Dataset', 32 | 'long_description': readme, 33 | 'long_description_content_type': "text/markdown", 34 | 'author': 'Marius Cordts', 35 | 'url': 'https://github.com/mcordts/cityscapesScripts', 36 | 'author_email': 'mail@cityscapes-dataset.net', 37 | 'license': 'https://github.com/mcordts/cityscapesScripts/blob/master/license.txt', 38 | 'version': '1.1.0', 39 | 'install_requires': ['numpy', 'matplotlib', 'pillow'], 40 | 'setup_requires': ['setuptools>=18.0'], 41 | 'packages': find_packages(), 42 | 'scripts': [], 43 | 'entry_points': {'gui_scripts': ['csViewer = cityscapesscripts.viewer.cityscapesViewer:main', 44 | 'csLabelTool = cityscapesscripts.annotation.cityscapesLabelTool:main'], 45 | 'console_scripts': ['csEvalPixelLevelSemanticLabeling = cityscapesscripts.evaluation.evalPixelLevelSemanticLabeling:main', 46 | 'csEvalInstanceLevelSemanticLabeling = cityscapesscripts.evaluation.evalInstanceLevelSemanticLabeling:main', 47 | 'csEvalPanopticSemanticLabeling = cityscapesscripts.evaluation.evalPanopticSemanticLabeling:main', 48 | 'csCreateTrainIdLabelImgs = cityscapesscripts.preparation.createTrainIdLabelImgs:main', 49 | 'csCreateTrainIdInstanceImgs = cityscapesscripts.preparation.createTrainIdInstanceImgs:main', 50 | 'csCreatePanopticImgs = cityscapesscripts.preparation.createPanopticImgs:main']}, 51 | 'package_data': {'': ['icons/*.png']}, 52 | 'ext_modules': ext_modules, 53 | 'include_dirs': include_dirs 54 | } 55 | 56 | setup(**config) 57 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/evaluation/instances2dict_with_polygons.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Convert instances from png files to a dictionary 4 | # This files is created according to https://github.com/facebookresearch/Detectron/issues/111 5 | 6 | from __future__ import print_function, absolute_import, division 7 | import os, sys 8 | 9 | sys.path.append( os.path.normpath( os.path.join( os.path.dirname( __file__ ) , '..' , 'helpers' ) ) ) 10 | from csHelpers import * 11 | 12 | # Cityscapes imports 13 | from cityscapesscripts.evaluation.instance import * 14 | from cityscapesscripts.helpers.csHelpers import * 15 | import cv2 16 | 17 | 18 | def instances2dict_with_polygons(imageFileList, verbose=False): 19 | imgCount = 0 20 | instanceDict = {} 21 | 22 | if not isinstance(imageFileList, list): 23 | imageFileList = [imageFileList] 24 | 25 | if verbose: 26 | print("Processing {} images...".format(len(imageFileList))) 27 | 28 | for imageFileName in imageFileList: 29 | # Load image 30 | img = Image.open(imageFileName) 31 | 32 | # Image as numpy array 33 | imgNp = np.array(img) 34 | 35 | # Initialize label categories 36 | instances = {} 37 | for label in labels: 38 | instances[label.name] = [] 39 | 40 | # Loop through all instance ids in instance image 41 | for instanceId in np.unique(imgNp): 42 | if instanceId < 1000: 43 | continue 44 | instanceObj = Instance(imgNp, instanceId) 45 | instanceObj_dict = instanceObj.toDict() 46 | 47 | #instances[id2label[instanceObj.labelID].name].append(instanceObj.toDict()) 48 | if id2label[instanceObj.labelID].hasInstances: 49 | mask = (imgNp == instanceId).astype(np.uint8) 50 | contour, hier = cv2.findContours( 51 | mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) 52 | 53 | polygons = [c.reshape(-1).tolist() for c in contour] 54 | instanceObj_dict['contours'] = polygons 55 | 56 | instances[id2label[instanceObj.labelID].name].append(instanceObj_dict) 57 | 58 | imgKey = os.path.abspath(imageFileName) 59 | instanceDict[imgKey] = instances 60 | imgCount += 1 61 | 62 | if verbose: 63 | print("\rImages Processed: {}".format(imgCount), end=' ') 64 | sys.stdout.flush() 65 | 66 | if verbose: 67 | print("") 68 | 69 | return instanceDict 70 | 71 | def main(argv): 72 | fileList = [] 73 | if (len(argv) > 2): 74 | for arg in argv: 75 | if ("png" in arg): 76 | fileList.append(arg) 77 | instances2dict_with_polygons(fileList, True) 78 | 79 | if __name__ == "__main__": 80 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/helpers/labels_cityPersons.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # CityPersons (cp) labels 4 | # 5 | 6 | from __future__ import print_function, absolute_import, division 7 | from collections import namedtuple 8 | 9 | 10 | #-------------------------------------------------------------------------------- 11 | # Definitions 12 | #-------------------------------------------------------------------------------- 13 | 14 | # a label and all meta information 15 | LabelCp = namedtuple( 'LabelCp' , [ 16 | 17 | 'name' , # The identifier of this label, e.g. 'pedestrian', 'rider', ... . 18 | # We use them to uniquely name a class 19 | 20 | 'id' , # An integer ID that is associated with this label. 21 | # The IDs are used to represent the label in ground truth 22 | 23 | 'hasInstances', # Whether this label distinguishes between single instances or not 24 | 25 | 'ignoreInEval', # Whether pixels having this class as ground truth label are ignored 26 | # during evaluations or not 27 | 28 | 'color' , # The color of this label 29 | ] ) 30 | 31 | 32 | #-------------------------------------------------------------------------------- 33 | # A list of all labels 34 | #-------------------------------------------------------------------------------- 35 | 36 | # The 'ignore' label covers representations of humans, e.g. people on posters, reflections etc. 37 | # Each annotation includes both the full bounding box (bbox) as well as a bounding box covering the visible area (bboxVis). 38 | # The latter is obtained automatically from the segmentation masks. 39 | 40 | labelsCp = [ 41 | # name id hasInstances ignoreInEval color 42 | LabelCp( 'ignore' , 0 , False , True , (250,170, 30) ), 43 | LabelCp( 'pedestrian' , 1 , True , False , (220, 20, 60) ), 44 | LabelCp( 'rider' , 2 , True , False , ( 0, 0,142) ), 45 | LabelCp( 'sitting person' , 3 , True , False , (107,142, 35) ), 46 | LabelCp( 'person (other)' , 4 , True , False , (190,153,153) ), 47 | LabelCp( 'person group' , 5 , False , True , (255, 0, 0) ), 48 | ] 49 | 50 | 51 | #-------------------------------------------------------------------------------- 52 | # Create dictionaries for a fast lookup 53 | #-------------------------------------------------------------------------------- 54 | 55 | # Please refer to the main method below for example usages! 56 | 57 | # name to label object 58 | name2labelCp = { label.name : label for label in labelsCp } 59 | # id to label object 60 | id2labelCp = { label.id : label for label in labelsCp } 61 | 62 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/preparation/createTrainIdLabelImgs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Converts the polygonal annotations of the Cityscapes dataset 4 | # to images, where pixel values encode ground truth classes. 5 | # 6 | # The Cityscapes downloads already include such images 7 | # a) *color.png : the class is encoded by its color 8 | # b) *labelIds.png : the class is encoded by its ID 9 | # c) *instanceIds.png : the class and the instance are encoded by an instance ID 10 | # 11 | # With this tool, you can generate option 12 | # d) *labelTrainIds.png : the class is encoded by its training ID 13 | # This encoding might come handy for training purposes. You can use 14 | # the file labels.py to define the training IDs that suit your needs. 15 | # Note however, that once you submit or evaluate results, the regular 16 | # IDs are needed. 17 | # 18 | # Uses the converter tool in 'json2labelImg.py' 19 | # Uses the mapping defined in 'labels.py' 20 | # 21 | 22 | # python imports 23 | from __future__ import print_function, absolute_import, division 24 | import os, glob, sys 25 | 26 | # cityscapes imports 27 | from cityscapesscripts.helpers.csHelpers import printError 28 | from cityscapesscripts.preparation.json2labelImg import json2labelImg 29 | 30 | # The main method 31 | def main(): 32 | # Where to look for Cityscapes 33 | if 'CITYSCAPES_DATASET' in os.environ: 34 | cityscapesPath = os.environ['CITYSCAPES_DATASET'] 35 | else: 36 | cityscapesPath = os.path.join(os.path.dirname(os.path.realpath(__file__)),'..','..') 37 | # how to search for all ground truth 38 | searchFine = os.path.join( cityscapesPath , "gtFine" , "*" , "*" , "*_gt*_polygons.json" ) 39 | searchCoarse = os.path.join( cityscapesPath , "gtCoarse" , "*" , "*" , "*_gt*_polygons.json" ) 40 | 41 | # search files 42 | filesFine = glob.glob( searchFine ) 43 | filesFine.sort() 44 | filesCoarse = glob.glob( searchCoarse ) 45 | filesCoarse.sort() 46 | 47 | # concatenate fine and coarse 48 | files = filesFine + filesCoarse 49 | # files = filesFine # use this line if fine is enough for now. 50 | 51 | # quit if we did not find anything 52 | if not files: 53 | printError( "Did not find any files. Please consult the README." ) 54 | 55 | # a bit verbose 56 | print("Processing {} annotation files".format(len(files))) 57 | 58 | # iterate through files 59 | progress = 0 60 | print("Progress: {:>3} %".format( progress * 100 / len(files) ), end=' ') 61 | for f in files: 62 | # create the output filename 63 | dst = f.replace( "_polygons.json" , "_labelTrainIds.png" ) 64 | 65 | # do the conversion 66 | try: 67 | json2labelImg( f , dst , "trainIds" ) 68 | except: 69 | print("Failed to convert: {}".format(f)) 70 | raise 71 | 72 | # status 73 | progress += 1 74 | print("\rProgress: {:>3} %".format( progress * 100 / len(files) ), end=' ') 75 | sys.stdout.flush() 76 | 77 | 78 | # call the main 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/preparation/createTrainIdInstanceImgs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Converts the polygonal annotations of the Cityscapes dataset 4 | # to images, where pixel values encode the ground truth classes and the 5 | # individual instance of that classes. 6 | # 7 | # The Cityscapes downloads already include such images 8 | # a) *color.png : the class is encoded by its color 9 | # b) *labelIds.png : the class is encoded by its ID 10 | # c) *instanceIds.png : the class and the instance are encoded by an instance ID 11 | # 12 | # With this tool, you can generate option 13 | # d) *instanceTrainIds.png : the class and the instance are encoded by an instance training ID 14 | # This encoding might come handy for training purposes. You can use 15 | # the file labes.py to define the training IDs that suit your needs. 16 | # Note however, that once you submit or evaluate results, the regular 17 | # IDs are needed. 18 | # 19 | # Please refer to 'json2instanceImg.py' for an explanation of instance IDs. 20 | # 21 | # Uses the converter tool in 'json2instanceImg.py' 22 | # Uses the mapping defined in 'labels.py' 23 | # 24 | 25 | # python imports 26 | from __future__ import print_function, absolute_import, division 27 | import os, glob, sys 28 | 29 | # cityscapes imports 30 | from cityscapesscripts.helpers.csHelpers import printError 31 | from cityscapesscripts.preparation.json2instanceImg import json2instanceImg 32 | 33 | 34 | # The main method 35 | def main(): 36 | # Where to look for Cityscapes 37 | if 'CITYSCAPES_DATASET' in os.environ: 38 | cityscapesPath = os.environ['CITYSCAPES_DATASET'] 39 | else: 40 | cityscapesPath = os.path.join(os.path.dirname(os.path.realpath(__file__)),'..','..') 41 | # how to search for all ground truth 42 | searchFine = os.path.join( cityscapesPath , "gtFine" , "*" , "*" , "*_gt*_polygons.json" ) 43 | searchCoarse = os.path.join( cityscapesPath , "gtCoarse" , "*" , "*" , "*_gt*_polygons.json" ) 44 | 45 | # search files 46 | filesFine = glob.glob( searchFine ) 47 | filesFine.sort() 48 | filesCoarse = glob.glob( searchCoarse ) 49 | filesCoarse.sort() 50 | 51 | # concatenate fine and coarse 52 | files = filesFine + filesCoarse 53 | # files = filesFine # use this line if fine is enough for now. 54 | 55 | # quit if we did not find anything 56 | if not files: 57 | printError( "Did not find any files. Please consult the README." ) 58 | 59 | # a bit verbose 60 | print("Processing {} annotation files".format(len(files))) 61 | 62 | # iterate through files 63 | progress = 0 64 | print("Progress: {:>3} %".format( progress * 100 / len(files) ), end=' ') 65 | for f in files: 66 | # create the output filename 67 | dst = f.replace( "_polygons.json" , "_instanceTrainIds.png" ) 68 | 69 | # do the conversion 70 | try: 71 | json2instanceImg( f , dst , "trainIds" ) 72 | except: 73 | print("Failed to convert: {}".format(f)) 74 | raise 75 | 76 | # status 77 | progress += 1 78 | print("\rProgress: {:>3} %".format( progress * 100 / len(files) ), end=' ') 79 | sys.stdout.flush() 80 | 81 | 82 | # call the main 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/helpers/csHelpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Various helper methods and includes for Cityscapes 4 | # 5 | 6 | # Python imports 7 | from __future__ import print_function, absolute_import, division 8 | import os, sys, getopt 9 | import glob 10 | import math 11 | import json 12 | from collections import namedtuple 13 | import logging 14 | import traceback 15 | 16 | # Image processing 17 | # Check if PIL is actually Pillow as expected 18 | try: 19 | from PIL import PILLOW_VERSION 20 | except: 21 | print("Please install the module 'Pillow' for image processing, e.g.") 22 | print("pip install pillow") 23 | sys.exit(-1) 24 | 25 | try: 26 | import PIL.Image as Image 27 | import PIL.ImageDraw as ImageDraw 28 | except: 29 | print("Failed to import the image processing packages.") 30 | sys.exit(-1) 31 | 32 | # Numpy for datastructures 33 | try: 34 | import numpy as np 35 | except: 36 | print("Failed to import numpy package.") 37 | sys.exit(-1) 38 | 39 | # Cityscapes modules 40 | try: 41 | from cityscapesscripts.helpers.annotation import Annotation 42 | from cityscapesscripts.helpers.labels import labels, name2label, id2label, trainId2label, category2labels 43 | except ImportError as err: 44 | print("Failed to import all Cityscapes modules: %s" % err) 45 | sys.exit(-1) 46 | except Exception as e: 47 | logging.error(traceback.format_exc()) 48 | sys.exit(-1) 49 | except: 50 | print("Unexpected error in loading Cityscapes modules") 51 | print(sys.exc_info()[0]) 52 | sys.exit(-1) 53 | 54 | # Print an error message and quit 55 | def printError(message): 56 | print('ERROR: ' + str(message)) 57 | sys.exit(-1) 58 | 59 | # Class for colors 60 | class colors: 61 | RED = '\033[31;1m' 62 | GREEN = '\033[32;1m' 63 | YELLOW = '\033[33;1m' 64 | BLUE = '\033[34;1m' 65 | MAGENTA = '\033[35;1m' 66 | CYAN = '\033[36;1m' 67 | BOLD = '\033[1m' 68 | UNDERLINE = '\033[4m' 69 | ENDC = '\033[0m' 70 | 71 | # Colored value output if colorized flag is activated. 72 | def getColorEntry(val, args): 73 | if not args.colorized: 74 | return "" 75 | if not isinstance(val, float) or math.isnan(val): 76 | return colors.ENDC 77 | if (val < .20): 78 | return colors.RED 79 | elif (val < .40): 80 | return colors.YELLOW 81 | elif (val < .60): 82 | return colors.BLUE 83 | elif (val < .80): 84 | return colors.CYAN 85 | else: 86 | return colors.GREEN 87 | 88 | # Cityscapes files have a typical filename structure 89 | # ___[_]. 90 | # This class contains the individual elements as members 91 | # For the sequence and frame number, the strings are returned, including leading zeros 92 | CsFile = namedtuple( 'csFile' , [ 'city' , 'sequenceNb' , 'frameNb' , 'type' , 'type2' , 'ext' ] ) 93 | 94 | # Returns a CsFile object filled from the info in the given filename 95 | def getCsFileInfo(fileName): 96 | baseName = os.path.basename(fileName) 97 | parts = baseName.split('_') 98 | parts = parts[:-1] + parts[-1].split('.') 99 | if not parts: 100 | printError( 'Cannot parse given filename ({}). Does not seem to be a valid Cityscapes file.'.format(fileName) ) 101 | if len(parts) == 5: 102 | csFile = CsFile( *parts[:-1] , type2="" , ext=parts[-1] ) 103 | elif len(parts) == 6: 104 | csFile = CsFile( *parts ) 105 | else: 106 | printError( 'Found {} part(s) in given filename ({}). Expected 5 or 6.'.format(len(parts) , fileName) ) 107 | 108 | return csFile 109 | 110 | # Returns the part of Cityscapes filenames that is common to all data types 111 | # e.g. for city_123456_123456_gtFine_polygons.json returns city_123456_123456 112 | def getCoreImageFileName(filename): 113 | csFile = getCsFileInfo(filename) 114 | return "{}_{}_{}".format( csFile.city , csFile.sequenceNb , csFile.frameNb ) 115 | 116 | # Returns the directory name for the given filename, e.g. 117 | # fileName = "/foo/bar/foobar.txt" 118 | # return value is "bar" 119 | # Not much error checking though 120 | def getDirectory(fileName): 121 | dirName = os.path.dirname(fileName) 122 | return os.path.basename(dirName) 123 | 124 | # Make sure that the given path exists 125 | def ensurePath(path): 126 | if not path: 127 | return 128 | if not os.path.isdir(path): 129 | os.makedirs(path) 130 | 131 | # Write a dictionary as json file 132 | def writeDict2JSON(dictName, fileName): 133 | with open(fileName, 'w') as f: 134 | f.write(json.dumps(dictName, default=lambda o: o.__dict__, sort_keys=True, indent=4)) 135 | 136 | # dummy main 137 | if __name__ == "__main__": 138 | printError("Only for include, not executable on its own.") 139 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/preparation/json2labelImg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Reads labels as polygons in JSON format and converts them to label images, 4 | # where each pixel has an ID that represents the ground truth label. 5 | # 6 | # Usage: json2labelImg.py [OPTIONS] 7 | # Options: 8 | # -h print a little help text 9 | # -t use train IDs 10 | # 11 | # Can also be used by including as a module. 12 | # 13 | # Uses the mapping defined in 'labels.py'. 14 | # 15 | # See also createTrainIdLabelImgs.py to apply the mapping to all annotations in Cityscapes. 16 | # 17 | 18 | # python imports 19 | from __future__ import print_function, absolute_import, division 20 | import os, sys, getopt 21 | 22 | # Image processing 23 | # Check if PIL is actually Pillow as expected 24 | try: 25 | from PIL import PILLOW_VERSION 26 | except: 27 | print("Please install the module 'Pillow' for image processing, e.g.") 28 | print("pip install pillow") 29 | sys.exit(-1) 30 | 31 | try: 32 | import PIL.Image as Image 33 | import PIL.ImageDraw as ImageDraw 34 | except: 35 | print("Failed to import the image processing packages.") 36 | sys.exit(-1) 37 | 38 | 39 | # cityscapes imports 40 | from cityscapesscripts.helpers.annotation import Annotation 41 | from cityscapesscripts.helpers.labels import name2label 42 | 43 | # Print the information 44 | def printHelp(): 45 | print('{} [OPTIONS] inputJson outputImg'.format(os.path.basename(sys.argv[0]))) 46 | print('') 47 | print('Reads labels as polygons in JSON format and converts them to label images,') 48 | print('where each pixel has an ID that represents the ground truth label.') 49 | print('') 50 | print('Options:') 51 | print(' -h Print this help') 52 | print(' -t Use the "trainIDs" instead of the regular mapping. See "labels.py" for details.') 53 | 54 | # Print an error message and quit 55 | def printError(message): 56 | print('ERROR: {}'.format(message)) 57 | print('') 58 | print('USAGE:') 59 | printHelp() 60 | sys.exit(-1) 61 | 62 | # Convert the given annotation to a label image 63 | def createLabelImage(annotation, encoding, outline=None): 64 | # the size of the image 65 | size = ( annotation.imgWidth , annotation.imgHeight ) 66 | 67 | # the background 68 | if encoding == "ids": 69 | background = name2label['unlabeled'].id 70 | elif encoding == "trainIds": 71 | background = name2label['unlabeled'].trainId 72 | elif encoding == "color": 73 | background = name2label['unlabeled'].color 74 | else: 75 | print("Unknown encoding '{}'".format(encoding)) 76 | return None 77 | 78 | # this is the image that we want to create 79 | if encoding == "color": 80 | labelImg = Image.new("RGBA", size, background) 81 | else: 82 | labelImg = Image.new("L", size, background) 83 | 84 | # a drawer to draw into the image 85 | drawer = ImageDraw.Draw( labelImg ) 86 | 87 | # loop over all objects 88 | for obj in annotation.objects: 89 | label = obj.label 90 | polygon = obj.polygon 91 | 92 | # If the object is deleted, skip it 93 | if obj.deleted: 94 | continue 95 | 96 | # If the label is not known, but ends with a 'group' (e.g. cargroup) 97 | # try to remove the s and see if that works 98 | if ( not label in name2label ) and label.endswith('group'): 99 | label = label[:-len('group')] 100 | 101 | if not label in name2label: 102 | printError( "Label '{}' not known.".format(label) ) 103 | 104 | # If the ID is negative that polygon should not be drawn 105 | if name2label[label].id < 0: 106 | continue 107 | 108 | if encoding == "ids": 109 | val = name2label[label].id 110 | elif encoding == "trainIds": 111 | val = name2label[label].trainId 112 | elif encoding == "color": 113 | val = name2label[label].color 114 | 115 | try: 116 | if outline: 117 | drawer.polygon( polygon, fill=val, outline=outline ) 118 | else: 119 | drawer.polygon( polygon, fill=val ) 120 | except: 121 | print("Failed to draw polygon with label {}".format(label)) 122 | raise 123 | 124 | return labelImg 125 | 126 | # A method that does all the work 127 | # inJson is the filename of the json file 128 | # outImg is the filename of the label image that is generated 129 | # encoding can be set to 130 | # - "ids" : classes are encoded using the regular label IDs 131 | # - "trainIds" : classes are encoded using the training IDs 132 | # - "color" : classes are encoded using the corresponding colors 133 | def json2labelImg(inJson,outImg,encoding="ids"): 134 | annotation = Annotation() 135 | annotation.fromJsonFile(inJson) 136 | labelImg = createLabelImage( annotation , encoding ) 137 | labelImg.save( outImg ) 138 | 139 | # The main method, if you execute this script directly 140 | # Reads the command line arguments and calls the method 'json2labelImg' 141 | def main(argv): 142 | trainIds = False 143 | try: 144 | opts, args = getopt.getopt(argv,"ht") 145 | except getopt.GetoptError: 146 | printError( 'Invalid arguments' ) 147 | for opt, arg in opts: 148 | if opt == '-h': 149 | printHelp() 150 | sys.exit(0) 151 | elif opt == '-t': 152 | trainIds = True 153 | else: 154 | printError( "Handling of argument '{}' not implementend".format(opt) ) 155 | 156 | if len(args) == 0: 157 | printError( "Missing input json file" ) 158 | elif len(args) == 1: 159 | printError( "Missing output image filename" ) 160 | elif len(args) > 2: 161 | printError( "Too many arguments" ) 162 | 163 | inJson = args[0] 164 | outImg = args[1] 165 | 166 | if trainIds: 167 | json2labelImg( inJson , outImg , "trainIds" ) 168 | else: 169 | json2labelImg( inJson , outImg ) 170 | 171 | # call the main method 172 | if __name__ == "__main__": 173 | main(sys.argv[1:]) 174 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/preparation/createPanopticImgs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Converts the *instanceIds.png annotations of the Cityscapes dataset 4 | # to COCO-style panoptic segmentation format (http://cocodataset.org/#format-data). 5 | # The convertion is working for 'fine' set of the annotations. 6 | # 7 | # By default with this tool uses IDs specified in labels.py. You can use flag 8 | # --use-train-id to get train ids for categories. 'ignoreInEval' categories are 9 | # removed during the conversion. 10 | # 11 | # In panoptic segmentation format image_id is used to match predictions and ground truth. 12 | # For cityscapes image_id has form _123456_123456 and corresponds to the prefix 13 | # of cityscapes image files. 14 | # 15 | 16 | # python imports 17 | from __future__ import print_function, absolute_import, division, unicode_literals 18 | import os 19 | import glob 20 | import sys 21 | import argparse 22 | import json 23 | import numpy as np 24 | 25 | # Image processing 26 | # Check if PIL is actually Pillow as expected 27 | try: 28 | from PIL import PILLOW_VERSION 29 | except ImportError: 30 | print("Please install the module 'Pillow' for image processing, e.g.") 31 | print("pip install pillow") 32 | sys.exit(-1) 33 | 34 | try: 35 | import PIL.Image as Image 36 | except ImportError: 37 | print("Failed to import the image processing packages.") 38 | sys.exit(-1) 39 | 40 | # cityscapes imports 41 | from cityscapesscripts.helpers.csHelpers import printError 42 | from cityscapesscripts.helpers.labels import id2label, labels 43 | 44 | 45 | # The main method 46 | def convert2panoptic(cityscapesPath=None, outputFolder=None, useTrainId=False): 47 | # Where to look for Cityscapes 48 | if cityscapesPath is None: 49 | if 'CITYSCAPES_DATASET' in os.environ: 50 | cityscapesPath = os.environ['CITYSCAPES_DATASET'] 51 | else: 52 | cityscapesPath = os.path.join(os.path.dirname(os.path.realpath(__file__)),'..','..') 53 | cityscapesPath = os.path.join(cityscapesPath, "gtFine") 54 | 55 | if outputFolder is None: 56 | outputFolder = cityscapesPath 57 | 58 | categories = [] 59 | for label in labels: 60 | if label.ignoreInEval: 61 | continue 62 | categories.append({'id': int(label.trainId) if useTrainId else int(label.id), 63 | 'name': label.name, 64 | 'color': label.color, 65 | 'supercategory': label.category, 66 | 'isthing': 1 if label.hasInstances else 0}) 67 | 68 | # if only val set needs the conversion. 69 | # for setName in ["val"]: 70 | for setName in ["val", "train", "test"]: 71 | # how to search for all ground truth 72 | searchFine = os.path.join(cityscapesPath, setName, "*", "*_instanceIds.png") 73 | # search files 74 | filesFine = glob.glob(searchFine) 75 | filesFine.sort() 76 | 77 | files = filesFine 78 | # quit if we did not find anything 79 | if not files: 80 | printError( 81 | "Did not find any files for {} set using matching pattern {}. Please consult the README.".format(setName, searchFine) 82 | ) 83 | # a bit verbose 84 | print("Converting {} annotation files for {} set.".format(len(files), setName)) 85 | 86 | trainIfSuffix = "_trainId" if useTrainId else "" 87 | outputBaseFile = "cityscapes_panoptic_{}{}".format(setName, trainIfSuffix) 88 | outFile = os.path.join(outputFolder, "{}.json".format(outputBaseFile)) 89 | print("Json file with the annotations in panoptic format will be saved in {}".format(outFile)) 90 | panopticFolder = os.path.join(outputFolder, outputBaseFile) 91 | if not os.path.isdir(panopticFolder): 92 | print("Creating folder {} for panoptic segmentation PNGs".format(panopticFolder)) 93 | os.mkdir(panopticFolder) 94 | print("Corresponding segmentations in .png format will be saved in {}".format(panopticFolder)) 95 | 96 | images = [] 97 | annotations = [] 98 | for progress, f in enumerate(files): 99 | 100 | originalFormat = np.array(Image.open(f)) 101 | 102 | fileName = os.path.basename(f) 103 | imageId = fileName.replace("_gtFine_instanceIds.png", "") 104 | inputFileName = fileName.replace("_instanceIds.png", "_leftImg8bit.png") 105 | outputFileName = fileName.replace("_instanceIds.png", "_panoptic.png") 106 | # image entry, id for image is its filename without extension 107 | images.append({"id": imageId, 108 | "width": int(originalFormat.shape[1]), 109 | "height": int(originalFormat.shape[0]), 110 | "file_name": inputFileName}) 111 | 112 | pan_format = np.zeros( 113 | (originalFormat.shape[0], originalFormat.shape[1], 3), dtype=np.uint8 114 | ) 115 | 116 | segmentIds = np.unique(originalFormat) 117 | segmInfo = [] 118 | for segmentId in segmentIds: 119 | if segmentId < 1000: 120 | semanticId = segmentId 121 | isCrowd = 1 122 | else: 123 | semanticId = segmentId // 1000 124 | isCrowd = 0 125 | labelInfo = id2label[semanticId] 126 | categoryId = labelInfo.trainId if useTrainId else labelInfo.id 127 | if labelInfo.ignoreInEval: 128 | continue 129 | if not labelInfo.hasInstances: 130 | isCrowd = 0 131 | 132 | mask = originalFormat == segmentId 133 | color = [segmentId % 256, segmentId // 256, segmentId // 256 // 256] 134 | pan_format[mask] = color 135 | 136 | area = np.sum(mask) # segment area computation 137 | 138 | # bbox computation for a segment 139 | hor = np.sum(mask, axis=0) 140 | hor_idx = np.nonzero(hor)[0] 141 | x = hor_idx[0] 142 | width = hor_idx[-1] - x + 1 143 | vert = np.sum(mask, axis=1) 144 | vert_idx = np.nonzero(vert)[0] 145 | y = vert_idx[0] 146 | height = vert_idx[-1] - y + 1 147 | bbox = [int(x), int(y), int(width), int(height)] 148 | 149 | segmInfo.append({"id": int(segmentId), 150 | "category_id": int(categoryId), 151 | "area": int(area), 152 | "bbox": bbox, 153 | "iscrowd": isCrowd}) 154 | 155 | annotations.append({'image_id': imageId, 156 | 'file_name': outputFileName, 157 | "segments_info": segmInfo}) 158 | 159 | Image.fromarray(pan_format).save(os.path.join(panopticFolder, outputFileName)) 160 | 161 | print("\rProgress: {:>3.2f} %".format((progress + 1) * 100 / len(files)), end=' ') 162 | sys.stdout.flush() 163 | 164 | print("\nSaving the json file {}".format(outFile)) 165 | d = {'images': images, 166 | 'annotations': annotations, 167 | 'categories': categories} 168 | with open(outFile, 'w') as f: 169 | json.dump(d, f, sort_keys=True, indent=4) 170 | 171 | 172 | def main(): 173 | parser = argparse.ArgumentParser() 174 | parser.add_argument("--dataset-folder", 175 | dest="cityscapesPath", 176 | help="path to the Cityscapes dataset 'gtFine' folder", 177 | default=None, 178 | type=str) 179 | parser.add_argument("--output-folder", 180 | dest="outputFolder", 181 | help="path to the output folder.", 182 | default=None, 183 | type=str) 184 | parser.add_argument("--use-train-id", action="store_true", dest="useTrainId") 185 | args = parser.parse_args() 186 | 187 | convert2panoptic(args.cityscapesPath, args.outputFolder, args.useTrainId) 188 | 189 | 190 | # call the main 191 | if __name__ == "__main__": 192 | main() 193 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/preparation/json2instanceImg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Reads labels as polygons in JSON format and converts them to instance images, 4 | # where each pixel has an ID that represents the ground truth class and the 5 | # individual instance of that class. 6 | # 7 | # The pixel values encode both, class and the individual instance. 8 | # The integer part of a division by 1000 of each ID provides the class ID, 9 | # as described in labels.py. The remainder is the instance ID. If a certain 10 | # annotation describes multiple instances, then the pixels have the regular 11 | # ID of that class. 12 | # 13 | # Example: 14 | # Let's say your labels.py assigns the ID 26 to the class 'car'. 15 | # Then, the individual cars in an image get the IDs 26000, 26001, 26002, ... . 16 | # A group of cars, where our annotators could not identify the individual 17 | # instances anymore, is assigned to the ID 26. 18 | # 19 | # Note that not all classes distinguish instances (see labels.py for a full list). 20 | # The classes without instance annotations are always directly encoded with 21 | # their regular ID, e.g. 11 for 'building'. 22 | # 23 | # Usage: json2instanceImg.py [OPTIONS] 24 | # Options: 25 | # -h print a little help text 26 | # -t use train IDs 27 | # 28 | # Can also be used by including as a module. 29 | # 30 | # Uses the mapping defined in 'labels.py'. 31 | # 32 | # See also createTrainIdInstanceImgs.py to apply the mapping to all annotations in Cityscapes. 33 | # 34 | 35 | # python imports 36 | from __future__ import print_function, absolute_import, division 37 | import os, sys, getopt 38 | 39 | # Image processing 40 | # Check if PIL is actually Pillow as expected 41 | try: 42 | from PIL import PILLOW_VERSION 43 | except: 44 | print("Please install the module 'Pillow' for image processing, e.g.") 45 | print("pip install pillow") 46 | sys.exit(-1) 47 | 48 | try: 49 | import PIL.Image as Image 50 | import PIL.ImageDraw as ImageDraw 51 | except: 52 | print("Failed to import the image processing packages.") 53 | sys.exit(-1) 54 | 55 | 56 | # cityscapes imports 57 | from cityscapesscripts.helpers.annotation import Annotation 58 | from cityscapesscripts.helpers.labels import labels, name2label 59 | 60 | # Print the information 61 | def printHelp(): 62 | print('{} [OPTIONS] inputJson outputImg'.format(os.path.basename(sys.argv[0]))) 63 | print('') 64 | print(' Reads labels as polygons in JSON format and converts them to instance images,') 65 | print(' where each pixel has an ID that represents the ground truth class and the') 66 | print(' individual instance of that class.') 67 | print('') 68 | print(' The pixel values encode both, class and the individual instance.') 69 | print(' The integer part of a division by 1000 of each ID provides the class ID,') 70 | print(' as described in labels.py. The remainder is the instance ID. If a certain') 71 | print(' annotation describes multiple instances, then the pixels have the regular') 72 | print(' ID of that class.') 73 | print('') 74 | print(' Example:') 75 | print(' Let\'s say your labels.py assigns the ID 26 to the class "car".') 76 | print(' Then, the individual cars in an image get the IDs 26000, 26001, 26002, ... .') 77 | print(' A group of cars, where our annotators could not identify the individual') 78 | print(' instances anymore, is assigned to the ID 26.') 79 | print('') 80 | print(' Note that not all classes distinguish instances (see labels.py for a full list).') 81 | print(' The classes without instance annotations are always directly encoded with') 82 | print(' their regular ID, e.g. 11 for "building".') 83 | print('') 84 | print('Options:') 85 | print(' -h Print this help') 86 | print(' -t Use the "trainIDs" instead of the regular mapping. See "labels.py" for details.') 87 | 88 | # Print an error message and quit 89 | def printError(message): 90 | print('ERROR: {}'.format(message)) 91 | print('') 92 | print('USAGE:') 93 | printHelp() 94 | sys.exit(-1) 95 | 96 | # Convert the given annotation to a label image 97 | def createInstanceImage(annotation, encoding): 98 | # the size of the image 99 | size = ( annotation.imgWidth , annotation.imgHeight ) 100 | 101 | # the background 102 | if encoding == "ids": 103 | backgroundId = name2label['unlabeled'].id 104 | elif encoding == "trainIds": 105 | backgroundId = name2label['unlabeled'].trainId 106 | else: 107 | print("Unknown encoding '{}'".format(encoding)) 108 | return None 109 | 110 | # this is the image that we want to create 111 | instanceImg = Image.new("I", size, backgroundId) 112 | 113 | # a drawer to draw into the image 114 | drawer = ImageDraw.Draw( instanceImg ) 115 | 116 | # a dict where we keep track of the number of instances that 117 | # we already saw of each class 118 | nbInstances = {} 119 | for labelTuple in labels: 120 | if labelTuple.hasInstances: 121 | nbInstances[labelTuple.name] = 0 122 | 123 | # loop over all objects 124 | for obj in annotation.objects: 125 | label = obj.label 126 | polygon = obj.polygon 127 | 128 | # If the object is deleted, skip it 129 | if obj.deleted: 130 | continue 131 | 132 | # if the label is not known, but ends with a 'group' (e.g. cargroup) 133 | # try to remove the s and see if that works 134 | # also we know that this polygon describes a group 135 | isGroup = False 136 | if ( not label in name2label ) and label.endswith('group'): 137 | label = label[:-len('group')] 138 | isGroup = True 139 | 140 | if not label in name2label: 141 | printError( "Label '{}' not known.".format(label) ) 142 | 143 | # the label tuple 144 | labelTuple = name2label[label] 145 | 146 | # get the class ID 147 | if encoding == "ids": 148 | id = labelTuple.id 149 | elif encoding == "trainIds": 150 | id = labelTuple.trainId 151 | 152 | # if this label distinguishs between individual instances, 153 | # make the id a instance ID 154 | if labelTuple.hasInstances and not isGroup and id != 255: 155 | id = id * 1000 + nbInstances[label] 156 | nbInstances[label] += 1 157 | 158 | # If the ID is negative that polygon should not be drawn 159 | if id < 0: 160 | continue 161 | 162 | try: 163 | drawer.polygon( polygon, fill=id ) 164 | except: 165 | print("Failed to draw polygon with label {} and id {}: {}".format(label,id,polygon)) 166 | raise 167 | 168 | return instanceImg 169 | 170 | # A method that does all the work 171 | # inJson is the filename of the json file 172 | # outImg is the filename of the instance image that is generated 173 | # encoding can be set to 174 | # - "ids" : classes are encoded using the regular label IDs 175 | # - "trainIds" : classes are encoded using the training IDs 176 | def json2instanceImg(inJson,outImg,encoding="ids"): 177 | annotation = Annotation() 178 | annotation.fromJsonFile(inJson) 179 | instanceImg = createInstanceImage( annotation , encoding ) 180 | instanceImg.save( outImg ) 181 | 182 | # The main method, if you execute this script directly 183 | # Reads the command line arguments and calls the method 'json2instanceImg' 184 | def main(argv): 185 | trainIds = False 186 | try: 187 | opts, args = getopt.getopt(argv,"ht") 188 | except getopt.GetoptError: 189 | printError( 'Invalid arguments' ) 190 | for opt, arg in opts: 191 | if opt == '-h': 192 | printHelp() 193 | sys.exit(0) 194 | elif opt == '-t': 195 | trainIds = True 196 | else: 197 | printError( "Handling of argument '{}' not implementend".format(opt) ) 198 | 199 | if len(args) == 0: 200 | printError( "Missing input json file" ) 201 | elif len(args) == 1: 202 | printError( "Missing output image filename" ) 203 | elif len(args) > 2: 204 | printError( "Too many arguments" ) 205 | 206 | inJson = args[0] 207 | outImg = args[1] 208 | 209 | if trainIds: 210 | json2instanceImg( inJson , outImg , 'trainIds' ) 211 | else: 212 | json2instanceImg( inJson , outImg ) 213 | 214 | # call the main method 215 | if __name__ == "__main__": 216 | main(sys.argv[1:]) 217 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/helpers/annotation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Classes to store, read, and write annotations 4 | # 5 | 6 | from __future__ import print_function, absolute_import, division 7 | import os 8 | import json 9 | from collections import namedtuple 10 | 11 | # get current date and time 12 | import datetime 13 | import locale 14 | 15 | # A point in a polygon 16 | Point = namedtuple('Point', ['x', 'y']) 17 | 18 | from abc import ABCMeta, abstractmethod 19 | 20 | # Type of an object 21 | class CsObjectType(): 22 | POLY = 1 # polygon 23 | BBOX = 2 # bounding box 24 | 25 | # Abstract base class for annotation objects 26 | class CsObject: 27 | __metaclass__ = ABCMeta 28 | 29 | def __init__(self, objType): 30 | self.objectType = objType 31 | # the label 32 | self.label = "" 33 | 34 | # If deleted or not 35 | self.deleted = 0 36 | # If verified or not 37 | self.verified = 0 38 | # The date string 39 | self.date = "" 40 | # The username 41 | self.user = "" 42 | # Draw the object 43 | # Not read from or written to JSON 44 | # Set to False if deleted object 45 | # Might be set to False by the application for other reasons 46 | self.draw = True 47 | 48 | @abstractmethod 49 | def __str__(self): pass 50 | 51 | @abstractmethod 52 | def fromJsonText(self, jsonText, objId=-1): pass 53 | 54 | @abstractmethod 55 | def toJsonText(self): pass 56 | 57 | def updateDate( self ): 58 | try: 59 | locale.setlocale( locale.LC_ALL , 'en_US.utf8' ) 60 | except locale.Error: 61 | locale.setlocale( locale.LC_ALL , 'en_US' ) 62 | except locale.Error: 63 | locale.setlocale( locale.LC_ALL , 'us_us.utf8' ) 64 | except locale.Error: 65 | locale.setlocale( locale.LC_ALL , 'us_us' ) 66 | except: 67 | pass 68 | self.date = datetime.datetime.now().strftime("%d-%b-%Y %H:%M:%S") 69 | 70 | # Mark the object as deleted 71 | def delete(self): 72 | self.deleted = 1 73 | self.draw = False 74 | 75 | # Class that contains the information of a single annotated object as polygon 76 | class CsPoly(CsObject): 77 | # Constructor 78 | def __init__(self): 79 | CsObject.__init__(self, CsObjectType.POLY) 80 | # the polygon as list of points 81 | self.polygon = [] 82 | # the object ID 83 | self.id = -1 84 | 85 | def __str__(self): 86 | polyText = "" 87 | if self.polygon: 88 | if len(self.polygon) <= 4: 89 | for p in self.polygon: 90 | polyText += '({},{}) '.format( p.x , p.y ) 91 | else: 92 | polyText += '({},{}) ({},{}) ... ({},{}) ({},{})'.format( 93 | self.polygon[ 0].x , self.polygon[ 0].y , 94 | self.polygon[ 1].x , self.polygon[ 1].y , 95 | self.polygon[-2].x , self.polygon[-2].y , 96 | self.polygon[-1].x , self.polygon[-1].y ) 97 | else: 98 | polyText = "none" 99 | text = "Object: {} - {}".format( self.label , polyText ) 100 | return text 101 | 102 | def fromJsonText(self, jsonText, objId): 103 | self.id = objId 104 | self.label = str(jsonText['label']) 105 | self.polygon = [ Point(p[0],p[1]) for p in jsonText['polygon'] ] 106 | if 'deleted' in jsonText.keys(): 107 | self.deleted = jsonText['deleted'] 108 | else: 109 | self.deleted = 0 110 | if 'verified' in jsonText.keys(): 111 | self.verified = jsonText['verified'] 112 | else: 113 | self.verified = 1 114 | if 'user' in jsonText.keys(): 115 | self.user = jsonText['user'] 116 | else: 117 | self.user = '' 118 | if 'date' in jsonText.keys(): 119 | self.date = jsonText['date'] 120 | else: 121 | self.date = '' 122 | if self.deleted == 1: 123 | self.draw = False 124 | else: 125 | self.draw = True 126 | 127 | def toJsonText(self): 128 | objDict = {} 129 | objDict['label'] = self.label 130 | objDict['id'] = self.id 131 | objDict['deleted'] = self.deleted 132 | objDict['verified'] = self.verified 133 | objDict['user'] = self.user 134 | objDict['date'] = self.date 135 | objDict['polygon'] = [] 136 | for pt in self.polygon: 137 | objDict['polygon'].append([pt.x, pt.y]) 138 | 139 | return objDict 140 | 141 | # Class that contains the information of a single annotated object as bounding box 142 | class CsBbox(CsObject): 143 | # Constructor 144 | def __init__(self): 145 | CsObject.__init__(self, CsObjectType.BBOX) 146 | # the polygon as list of points 147 | self.bbox = [] 148 | self.bboxVis = [] 149 | 150 | # the ID of the corresponding object 151 | self.instanceId = -1 152 | 153 | def __str__(self): 154 | bboxText = "" 155 | bboxText += '[(x1: {}, y1: {}), (w: {}, h: {})]'.format( 156 | self.bbox[0] , self.bbox[1] , self.bbox[2] , self.bbox[3] ) 157 | 158 | bboxVisText = "" 159 | bboxVisText += '[(x1: {}, y1: {}), (w: {}, h: {})]'.format( 160 | self.bboxVis[0] , self.bboxVis[1] , self.bboxVis[2], self.bboxVis[3] ) 161 | 162 | text = "Object: {} - bbox {} - visible {}".format( self.label , bboxText, bboxVisText ) 163 | return text 164 | 165 | def fromJsonText(self, jsonText, objId=-1): 166 | self.bbox = jsonText['bbox'] 167 | self.bboxVis = jsonText['bboxVis'] 168 | self.label = str(jsonText['label']) 169 | self.instanceId = jsonText['instanceId'] 170 | 171 | def toJsonText(self): 172 | objDict = {} 173 | objDict['label'] = self.label 174 | objDict['instanceId'] = self.instanceId 175 | objDict['bbox'] = self.bbox 176 | objDict['bboxVis'] = self.bboxVis 177 | 178 | return objDict 179 | 180 | # The annotation of a whole image (doesn't support mixed annotations, i.e. combining CsPoly and CsBbox) 181 | class Annotation: 182 | # Constructor 183 | def __init__(self, objType=CsObjectType.POLY): 184 | # the width of that image and thus of the label image 185 | self.imgWidth = 0 186 | # the height of that image and thus of the label image 187 | self.imgHeight = 0 188 | # the list of objects 189 | self.objects = [] 190 | assert objType in CsObjectType.__dict__.values() 191 | self.objectType = objType 192 | 193 | def toJson(self): 194 | return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) 195 | 196 | def fromJsonText(self, jsonText): 197 | jsonDict = json.loads(jsonText) 198 | self.imgWidth = int(jsonDict['imgWidth']) 199 | self.imgHeight = int(jsonDict['imgHeight']) 200 | self.objects = [] 201 | for objId, objIn in enumerate(jsonDict[ 'objects' ]): 202 | if self.objectType == CsObjectType.POLY: 203 | obj = CsPoly() 204 | elif self.objectType == CsObjectType.BBOX: 205 | obj = CsBbox() 206 | obj.fromJsonText(objIn, objId) 207 | self.objects.append(obj) 208 | 209 | def toJsonText(self): 210 | jsonDict = {} 211 | jsonDict['imgWidth'] = self.imgWidth 212 | jsonDict['imgHeight'] = self.imgHeight 213 | jsonDict['objects'] = [] 214 | for obj in self.objects: 215 | objDict = obj.toJsonText() 216 | jsonDict['objects'].append(objDict) 217 | 218 | return jsonDict 219 | 220 | # Read a json formatted polygon file and return the annotation 221 | def fromJsonFile(self, jsonFile): 222 | if not os.path.isfile(jsonFile): 223 | print('Given json file not found: {}'.format(jsonFile)) 224 | return 225 | with open(jsonFile, 'r') as f: 226 | jsonText = f.read() 227 | self.fromJsonText(jsonText) 228 | 229 | def toJsonFile(self, jsonFile): 230 | with open(jsonFile, 'w') as f: 231 | f.write(self.toJson()) 232 | 233 | 234 | # a dummy example 235 | if __name__ == "__main__": 236 | obj = CsPoly() 237 | obj.label = 'car' 238 | obj.polygon.append( Point( 0 , 0 ) ) 239 | obj.polygon.append( Point( 1 , 0 ) ) 240 | obj.polygon.append( Point( 1 , 1 ) ) 241 | obj.polygon.append( Point( 0 , 1 ) ) 242 | 243 | print(type(obj).__name__) 244 | print(obj) 245 | -------------------------------------------------------------------------------- /cityscapesScripts/README.md: -------------------------------------------------------------------------------- 1 | # The Cityscapes Dataset 2 | 3 | This repository contains scripts for inspection, preparation, and evaluation of the Cityscapes dataset. This large-scale dataset contains a diverse set of stereo video sequences recorded in street scenes from 50 different cities, with high quality pixel-level annotations of 5 000 frames in addition to a larger set of 20 000 weakly annotated frames. 4 | 5 | Details and download are available at: www.cityscapes-dataset.net 6 | 7 | 8 | ## Dataset Structure 9 | 10 | The folder structure of the Cityscapes dataset is as follows: 11 | ``` 12 | {root}/{type}{video}/{split}/{city}/{city}_{seq:0>6}_{frame:0>6}_{type}{ext} 13 | ``` 14 | 15 | The meaning of the individual elements is: 16 | - `root` the root folder of the Cityscapes dataset. Many of our scripts check if an environment variable `CITYSCAPES_DATASET` pointing to this folder exists and use this as the default choice. 17 | - `type` the type/modality of data, e.g. `gtFine` for fine ground truth, or `leftImg8bit` for left 8-bit images. 18 | - `split` the split, i.e. train/val/test/train_extra/demoVideo. Note that not all kinds of data exist for all splits. Thus, do not be surprised to occasionally find empty folders. 19 | - `city` the city in which this part of the dataset was recorded. 20 | - `seq` the sequence number using 6 digits. 21 | - `frame` the frame number using 6 digits. Note that in some cities very few, albeit very long sequences were recorded, while in some cities many short sequences were recorded, of which only the 19th frame is annotated. 22 | - `ext` the extension of the file and optionally a suffix, e.g. `_polygons.json` for ground truth files 23 | 24 | Possible values of `type` 25 | - `gtFine` the fine annotations, 2975 training, 500 validation, and 1525 testing. This type of annotations is used for validation, testing, and optionally for training. Annotations are encoded using `json` files containing the individual polygons. Additionally, we provide `png` images, where pixel values encode labels. Please refer to `helpers/labels.py` and the scripts in `preparation` for details. 26 | - `gtCoarse` the coarse annotations, available for all training and validation images and for another set of 19998 training images (`train_extra`). These annotations can be used for training, either together with gtFine or alone in a weakly supervised setup. 27 | - `gtBboxCityPersons` pedestrian bounding box annotations, available for all training and validation images. Please refer to `helpers/labels_cityPersons.py` as well as the [`CityPersons` publication (Zhang et al., CVPR '17)](https://bitbucket.org/shanshanzhang/citypersons) for more details. The four values of a bounding box are (x, y, w, h), where (x, y) is its top-left corner and (w, h) its width and height. 28 | - `leftImg8bit` the left images in 8-bit LDR format. These are the standard annotated images. 29 | - `leftImg8bit_blurred` the left images in 8-bit LDR format with faces and license plates blurred. Please compute results on the original images but use the blurred ones for visualization. We thank [Mapillary](https://www.mapillary.com/) for blurring the images. 30 | - `leftImg16bit` the left images in 16-bit HDR format. These images offer 16 bits per pixel of color depth and contain more information, especially in very dark or bright parts of the scene. Warning: The images are stored as 16-bit pngs, which is non-standard and not supported by all libraries. 31 | - `rightImg8bit` the right stereo views in 8-bit LDR format. 32 | - `rightImg16bit` the right stereo views in 16-bit HDR format. 33 | - `timestamp` the time of recording in ns. The first frame of each sequence always has a timestamp of 0. 34 | - `disparity` precomputed disparity depth maps. To obtain the disparity values, compute for each pixel p with p > 0: d = ( float(p) - 1. ) / 256., while a value p = 0 is an invalid measurement. Warning: the images are stored as 16-bit pngs, which is non-standard and not supported by all libraries. 35 | - `camera` internal and external camera calibration. For details, please refer to [csCalibration.pdf](docs/csCalibration.pdf) 36 | - `vehicle` vehicle odometry, GPS coordinates, and outside temperature. For details, please refer to [csCalibration.pdf](docs/csCalibration.pdf) 37 | 38 | More types might be added over time and also not all types are initially available. Please let us know if you need any other meta-data to run your approach. 39 | 40 | Possible values of `split` 41 | - `train` usually used for training, contains 2975 images with fine and coarse annotations 42 | - `val` should be used for validation of hyper-parameters, contains 500 image with fine and coarse annotations. Can also be used for training. 43 | - `test` used for testing on our evaluation server. The annotations are not public, but we include annotations of ego-vehicle and rectification border for convenience. 44 | - `train_extra` can be optionally used for training, contains 19998 images with coarse annotations 45 | - `demoVideo` video sequences that could be used for qualitative evaluation, no annotations are available for these videos 46 | 47 | 48 | ## Scripts 49 | 50 | There are several scripts included with the dataset in a folder named `scripts` 51 | - `helpers` helper files that are included by other scripts 52 | - `viewer` view the images and the annotations 53 | - `preparation` convert the ground truth annotations into a format suitable for your approach 54 | - `evaluation` validate your approach 55 | - `annotation` the annotation tool used for labeling the dataset 56 | 57 | 58 | Note that all files have a small documentation at the top. Most important files 59 | - `helpers/labels.py` central file defining the IDs of all semantic classes and providing mapping between various class properties. 60 | - `helpers/labels_cityPersons.py` file defining the IDs of all CityPersons pedestrian classes and providing mapping between various class properties. 61 | - `viewer/cityscapesViewer.py` view the images and overlay the annotations. 62 | - `preparation/createTrainIdLabelImgs.py` convert annotations in polygonal format to png images with label IDs, where pixels encode "train IDs" that you can define in `labels.py`. 63 | - `preparation/createTrainIdInstanceImgs.py` convert annotations in polygonal format to png images with instance IDs, where pixels encode instance IDs composed of "train IDs". 64 | - `preparation/createPanopticImgs.py` convert annotations in standard png format to [COCO panoptic segmentation format](http://cocodataset.org/#format-data). 65 | - `evaluation/evalPixelLevelSemanticLabeling.py` script to evaluate pixel-level semantic labeling results on the validation set. This script is also used to evaluate the results on the test set. 66 | - `evaluation/evalInstanceLevelSemanticLabeling.py` script to evaluate instance-level semantic labeling results on the validation set. This script is also used to evaluate the results on the test set. 67 | - `evaluation/evalPanopticSemanticLabeling.py` script to evaluate panoptic segmentation results on the validation set. This script is also used to evaluate the results on the test set. 68 | - `setup.py` run `CYTHONIZE_EVAL= python setup.py build_ext --inplace` to enable cython plugin for faster evaluation. Only tested for Ubuntu. 69 | 70 | The scripts can be installed via pip, i.e. from within the scripts: 71 | `sudo pip install .` 72 | This installs the scripts as a python module named `cityscapesscripts` and exposes the following tools, see above for descriptions: 73 | - `csViewer` 74 | - `csLabelTool` 75 | - `csEvalPixelLevelSemanticLabeling` 76 | - `csEvalInstanceLevelSemanticLabeling` 77 | - `csPanopticSemanticLabelling` 78 | - `csCreateTrainIdLabelImgs` 79 | - `csCreateTrainIdInstanceImgs` 80 | - `csCreatePanopticImgs` 81 | 82 | Note that for the grapical tools you additionally need to install: 83 | `sudo apt install python-tk python-qt4` 84 | 85 | 86 | ## Evaluation 87 | 88 | Once you want to test your method on the test set, please run your approach on the provided test images and submit your results: 89 | www.cityscapes-dataset.net/submit/ 90 | For semantic labeling, we require the result format to match the format of our label images named `labelIDs`. 91 | Thus, your code should produce images where each pixel's value corresponds to a class ID as defined in `labels.py`. 92 | Note that our evaluation scripts are included in the scripts folder and can be used to test your approach on the validation set. 93 | For further details regarding the submission process, please consult our website. 94 | 95 | ## Contact 96 | 97 | Please feel free to contact us with any questions, suggestions or comments: 98 | 99 | * Marius Cordts, Mohamed Omran 100 | * mail@cityscapes-dataset.net 101 | * www.cityscapes-dataset.net 102 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/helpers/labels.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Cityscapes labels 4 | # 5 | 6 | from __future__ import print_function, absolute_import, division 7 | from collections import namedtuple 8 | 9 | 10 | #-------------------------------------------------------------------------------- 11 | # Definitions 12 | #-------------------------------------------------------------------------------- 13 | 14 | # a label and all meta information 15 | Label = namedtuple( 'Label' , [ 16 | 17 | 'name' , # The identifier of this label, e.g. 'car', 'person', ... . 18 | # We use them to uniquely name a class 19 | 20 | 'id' , # An integer ID that is associated with this label. 21 | # The IDs are used to represent the label in ground truth images 22 | # An ID of -1 means that this label does not have an ID and thus 23 | # is ignored when creating ground truth images (e.g. license plate). 24 | # Do not modify these IDs, since exactly these IDs are expected by the 25 | # evaluation server. 26 | 27 | 'trainId' , # Feel free to modify these IDs as suitable for your method. Then create 28 | # ground truth images with train IDs, using the tools provided in the 29 | # 'preparation' folder. However, make sure to validate or submit results 30 | # to our evaluation server using the regular IDs above! 31 | # For trainIds, multiple labels might have the same ID. Then, these labels 32 | # are mapped to the same class in the ground truth images. For the inverse 33 | # mapping, we use the label that is defined first in the list below. 34 | # For example, mapping all void-type classes to the same ID in training, 35 | # might make sense for some approaches. 36 | # Max value is 255! 37 | 38 | 'category' , # The name of the category that this label belongs to 39 | 40 | 'categoryId' , # The ID of this category. Used to create ground truth images 41 | # on category level. 42 | 43 | 'hasInstances', # Whether this label distinguishes between single instances or not 44 | 45 | 'ignoreInEval', # Whether pixels having this class as ground truth label are ignored 46 | # during evaluations or not 47 | 48 | 'color' , # The color of this label 49 | ] ) 50 | 51 | 52 | #-------------------------------------------------------------------------------- 53 | # A list of all labels 54 | #-------------------------------------------------------------------------------- 55 | 56 | # Please adapt the train IDs as appropriate for your approach. 57 | # Note that you might want to ignore labels with ID 255 during training. 58 | # Further note that the current train IDs are only a suggestion. You can use whatever you like. 59 | # Make sure to provide your results using the original IDs and not the training IDs. 60 | # Note that many IDs are ignored in evaluation and thus you never need to predict these! 61 | 62 | labels = [ 63 | # name id trainId category catId hasInstances ignoreInEval color 64 | Label( 'unlabeled' , 0 , 255 , 'void' , 0 , False , True , ( 0, 0, 0) ), 65 | Label( 'ego vehicle' , 1 , 255 , 'void' , 0 , False , True , ( 0, 0, 0) ), 66 | Label( 'rectification border' , 2 , 255 , 'void' , 0 , False , True , ( 0, 0, 0) ), 67 | Label( 'out of roi' , 3 , 255 , 'void' , 0 , False , True , ( 0, 0, 0) ), 68 | Label( 'static' , 4 , 255 , 'void' , 0 , False , True , ( 0, 0, 0) ), 69 | Label( 'dynamic' , 5 , 255 , 'void' , 0 , False , True , (111, 74, 0) ), 70 | Label( 'ground' , 6 , 255 , 'void' , 0 , False , True , ( 81, 0, 81) ), 71 | Label( 'road' , 7 , 0 , 'flat' , 1 , False , False , (128, 64,128) ), 72 | Label( 'sidewalk' , 8 , 1 , 'flat' , 1 , False , False , (244, 35,232) ), 73 | Label( 'parking' , 9 , 255 , 'flat' , 1 , False , True , (250,170,160) ), 74 | Label( 'rail track' , 10 , 255 , 'flat' , 1 , False , True , (230,150,140) ), 75 | Label( 'building' , 11 , 2 , 'construction' , 2 , False , False , ( 70, 70, 70) ), 76 | Label( 'wall' , 12 , 3 , 'construction' , 2 , False , False , (102,102,156) ), 77 | Label( 'fence' , 13 , 4 , 'construction' , 2 , False , False , (190,153,153) ), 78 | Label( 'guard rail' , 14 , 255 , 'construction' , 2 , False , True , (180,165,180) ), 79 | Label( 'bridge' , 15 , 255 , 'construction' , 2 , False , True , (150,100,100) ), 80 | Label( 'tunnel' , 16 , 255 , 'construction' , 2 , False , True , (150,120, 90) ), 81 | Label( 'pole' , 17 , 5 , 'object' , 3 , False , False , (153,153,153) ), 82 | Label( 'polegroup' , 18 , 255 , 'object' , 3 , False , True , (153,153,153) ), 83 | Label( 'traffic light' , 19 , 6 , 'object' , 3 , False , False , (250,170, 30) ), 84 | Label( 'traffic sign' , 20 , 7 , 'object' , 3 , False , False , (220,220, 0) ), 85 | Label( 'vegetation' , 21 , 8 , 'nature' , 4 , False , False , (107,142, 35) ), 86 | Label( 'terrain' , 22 , 9 , 'nature' , 4 , False , False , (152,251,152) ), 87 | Label( 'sky' , 23 , 10 , 'sky' , 5 , False , False , ( 70,130,180) ), 88 | Label( 'person' , 24 , 11 , 'human' , 6 , True , False , (220, 20, 60) ), 89 | Label( 'rider' , 25 , 12 , 'human' , 6 , True , False , (255, 0, 0) ), 90 | Label( 'car' , 26 , 13 , 'vehicle' , 7 , True , False , ( 0, 0,142) ), 91 | Label( 'truck' , 27 , 14 , 'vehicle' , 7 , True , False , ( 0, 0, 70) ), 92 | Label( 'bus' , 28 , 15 , 'vehicle' , 7 , True , False , ( 0, 60,100) ), 93 | Label( 'caravan' , 29 , 255 , 'vehicle' , 7 , True , True , ( 0, 0, 90) ), 94 | Label( 'trailer' , 30 , 255 , 'vehicle' , 7 , True , True , ( 0, 0,110) ), 95 | Label( 'train' , 31 , 16 , 'vehicle' , 7 , True , False , ( 0, 80,100) ), 96 | Label( 'motorcycle' , 32 , 17 , 'vehicle' , 7 , True , False , ( 0, 0,230) ), 97 | Label( 'bicycle' , 33 , 18 , 'vehicle' , 7 , True , False , (119, 11, 32) ), 98 | Label( 'license plate' , -1 , -1 , 'vehicle' , 7 , False , True , ( 0, 0,142) ), 99 | ] 100 | 101 | 102 | #-------------------------------------------------------------------------------- 103 | # Create dictionaries for a fast lookup 104 | #-------------------------------------------------------------------------------- 105 | 106 | # Please refer to the main method below for example usages! 107 | 108 | # name to label object 109 | name2label = { label.name : label for label in labels } 110 | # id to label object 111 | id2label = { label.id : label for label in labels } 112 | # trainId to label object 113 | trainId2label = { label.trainId : label for label in reversed(labels) } 114 | # category to list of label objects 115 | category2labels = {} 116 | for label in labels: 117 | category = label.category 118 | if category in category2labels: 119 | category2labels[category].append(label) 120 | else: 121 | category2labels[category] = [label] 122 | 123 | #-------------------------------------------------------------------------------- 124 | # Assure single instance name 125 | #-------------------------------------------------------------------------------- 126 | 127 | # returns the label name that describes a single instance (if possible) 128 | # e.g. input | output 129 | # ---------------------- 130 | # car | car 131 | # cargroup | car 132 | # foo | None 133 | # foogroup | None 134 | # skygroup | None 135 | def assureSingleInstanceName( name ): 136 | # if the name is known, it is not a group 137 | if name in name2label: 138 | return name 139 | # test if the name actually denotes a group 140 | if not name.endswith("group"): 141 | return None 142 | # remove group 143 | name = name[:-len("group")] 144 | # test if the new name exists 145 | if not name in name2label: 146 | return None 147 | # test if the new name denotes a label that actually has instances 148 | if not name2label[name].hasInstances: 149 | return None 150 | # all good then 151 | return name 152 | 153 | #-------------------------------------------------------------------------------- 154 | # Main for testing 155 | #-------------------------------------------------------------------------------- 156 | 157 | # just a dummy main 158 | if __name__ == "__main__": 159 | # Print all the labels 160 | print("List of cityscapes labels:") 161 | print("") 162 | print(" {:>21} | {:>3} | {:>7} | {:>14} | {:>10} | {:>12} | {:>12}".format( 'name', 'id', 'trainId', 'category', 'categoryId', 'hasInstances', 'ignoreInEval' )) 163 | print(" " + ('-' * 98)) 164 | for label in labels: 165 | print(" {:>21} | {:>3} | {:>7} | {:>14} | {:>10} | {:>12} | {:>12}".format( label.name, label.id, label.trainId, label.category, label.categoryId, label.hasInstances, label.ignoreInEval )) 166 | print("") 167 | 168 | print("Example usages:") 169 | 170 | # Map from name to label 171 | name = 'car' 172 | id = name2label[name].id 173 | print("ID of label '{name}': {id}".format( name=name, id=id )) 174 | 175 | # Map from ID to label 176 | category = id2label[id].category 177 | print("Category of label with ID '{id}': {category}".format( id=id, category=category )) 178 | 179 | # Map from trainID to label 180 | trainId = 0 181 | name = trainId2label[trainId].name 182 | print("Name of label with trainID '{id}': {name}".format( id=trainId, name=name )) 183 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/evaluation/evalPanopticSemanticLabeling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # The evaluation script for panoptic segmentation (https://arxiv.org/abs/1801.00868). 4 | # We use this script to evaluate your approach on the test set. 5 | # You can use the script to evaluate on the validation set. 6 | # Test set evaluation assumes prediction use 'id' and not 'trainId' 7 | # for categories, i.e. 'person' id is 24. 8 | # 9 | # The script expects both ground truth and predictions to use COCO panoptic 10 | # segmentation format (http://cocodataset.org/#format-data and 11 | # http://cocodataset.org/#format-results respectively). The format has 'image_id' field to 12 | # match prediction and annotation. For cityscapes we assume that the 'image_id' has form 13 | # _123456_123456 and corresponds to the prefix of cityscapes image files. 14 | # 15 | # Note, that panoptic segmentaion in COCO format is not included in the basic dataset distribution. 16 | # To obtain ground truth in this format, please run script 'preparation/createPanopticImgs.py' 17 | # from this repo. The script is quite slow and it may take up to 5 minutes to convert val set. 18 | # 19 | 20 | # python imports 21 | from __future__ import print_function, absolute_import, division, unicode_literals 22 | import os 23 | import sys 24 | import argparse 25 | import functools 26 | import traceback 27 | import json 28 | import time 29 | import multiprocessing 30 | import numpy as np 31 | from collections import defaultdict 32 | 33 | # Image processing 34 | # Check if PIL is actually Pillow as expected 35 | try: 36 | from PIL import PILLOW_VERSION 37 | except ImportError: 38 | print("Please install the module 'Pillow' for image processing, e.g.") 39 | print("pip install pillow") 40 | sys.exit(-1) 41 | 42 | try: 43 | import PIL.Image as Image 44 | except ImportError: 45 | print("Failed to import the image processing packages.") 46 | sys.exit(-1) 47 | 48 | # Cityscapes imports 49 | from cityscapesscripts.helpers.csHelpers import printError 50 | from cityscapesscripts.helpers.labels import labels as csLabels 51 | 52 | 53 | OFFSET = 256 * 256 * 256 54 | VOID = 0 55 | 56 | 57 | # The decorator is used to prints an error trhown inside process 58 | def get_traceback(f): 59 | @functools.wraps(f) 60 | def wrapper(*args, **kwargs): 61 | try: 62 | return f(*args, **kwargs) 63 | except Exception as e: 64 | print('Caught exception in worker thread:') 65 | traceback.print_exc() 66 | raise e 67 | 68 | return wrapper 69 | 70 | 71 | def rgb2id(color): 72 | if isinstance(color, np.ndarray) and len(color.shape) == 3: 73 | if color.dtype == np.uint8: 74 | color = color.astype(np.int32) 75 | return color[:, :, 0] + 256 * color[:, :, 1] + 256 * 256 * color[:, :, 2] 76 | return int(color[0] + 256 * color[1] + 256 * 256 * color[2]) 77 | 78 | 79 | class PQStatCat(): 80 | def __init__(self): 81 | self.iou = 0.0 82 | self.tp = 0 83 | self.fp = 0 84 | self.fn = 0 85 | 86 | def __iadd__(self, pq_stat_cat): 87 | self.iou += pq_stat_cat.iou 88 | self.tp += pq_stat_cat.tp 89 | self.fp += pq_stat_cat.fp 90 | self.fn += pq_stat_cat.fn 91 | return self 92 | 93 | 94 | class PQStat(): 95 | def __init__(self): 96 | self.pq_per_cat = defaultdict(PQStatCat) 97 | 98 | def __getitem__(self, i): 99 | return self.pq_per_cat[i] 100 | 101 | def __iadd__(self, pq_stat): 102 | for label, pq_stat_cat in pq_stat.pq_per_cat.items(): 103 | self.pq_per_cat[label] += pq_stat_cat 104 | return self 105 | 106 | def pq_average(self, categories, isthing): 107 | pq, sq, rq, n = 0, 0, 0, 0 108 | per_class_results = {} 109 | for label, label_info in categories.items(): 110 | if isthing is not None: 111 | cat_isthing = label_info['isthing'] == 1 112 | if isthing != cat_isthing: 113 | continue 114 | iou = self.pq_per_cat[label].iou 115 | tp = self.pq_per_cat[label].tp 116 | fp = self.pq_per_cat[label].fp 117 | fn = self.pq_per_cat[label].fn 118 | if tp + fp + fn == 0: 119 | per_class_results[label] = {'pq': 0.0, 'sq': 0.0, 'rq': 0.0} 120 | continue 121 | n += 1 122 | pq_class = iou / (tp + 0.5 * fp + 0.5 * fn) 123 | sq_class = iou / tp if tp != 0 else 0 124 | rq_class = tp / (tp + 0.5 * fp + 0.5 * fn) 125 | per_class_results[label] = {'pq': pq_class, 'sq': sq_class, 'rq': rq_class} 126 | pq += pq_class 127 | sq += sq_class 128 | rq += rq_class 129 | 130 | return {'pq': pq / n, 'sq': sq / n, 'rq': rq / n, 'n': n}, per_class_results 131 | 132 | 133 | @get_traceback 134 | def pq_compute_single_core(proc_id, annotation_set, gt_folder, pred_folder, categories): 135 | pq_stat = PQStat() 136 | 137 | idx = 0 138 | for gt_ann, pred_ann in annotation_set: 139 | if idx % 30 == 0: 140 | print('Core: {}, {} from {} images processed'.format(proc_id, idx, len(annotation_set))) 141 | idx += 1 142 | 143 | pan_gt = np.array(Image.open(os.path.join(gt_folder, gt_ann['file_name'])), dtype=np.uint32) 144 | pan_gt = rgb2id(pan_gt) 145 | pan_pred = np.array(Image.open(os.path.join(pred_folder, pred_ann['file_name'])), dtype=np.uint32) 146 | pan_pred = rgb2id(pan_pred) 147 | 148 | gt_segms = {el['id']: el for el in gt_ann['segments_info']} 149 | pred_segms = {el['id']: el for el in pred_ann['segments_info']} 150 | 151 | # predicted segments area calculation + prediction sanity checks 152 | pred_labels_set = set(el['id'] for el in pred_ann['segments_info']) 153 | labels, labels_cnt = np.unique(pan_pred, return_counts=True) 154 | for label, label_cnt in zip(labels, labels_cnt): 155 | if label not in pred_segms: 156 | if label == VOID: 157 | continue 158 | raise KeyError('In the image with ID {} segment with ID {} is presented in PNG and not presented in JSON.'.format(gt_ann['image_id'], label)) 159 | pred_segms[label]['area'] = label_cnt 160 | pred_labels_set.remove(label) 161 | if pred_segms[label]['category_id'] not in categories: 162 | raise KeyError('In the image with ID {} segment with ID {} has unknown category_id {}.'.format(gt_ann['image_id'], label, pred_segms[label]['category_id'])) 163 | if len(pred_labels_set) != 0: 164 | raise KeyError('In the image with ID {} the following segment IDs {} are presented in JSON and not presented in PNG.'.format(gt_ann['image_id'], list(pred_labels_set))) 165 | 166 | # confusion matrix calculation 167 | pan_gt_pred = pan_gt.astype(np.uint64) * OFFSET + pan_pred.astype(np.uint64) 168 | gt_pred_map = {} 169 | labels, labels_cnt = np.unique(pan_gt_pred, return_counts=True) 170 | for label, intersection in zip(labels, labels_cnt): 171 | gt_id = label // OFFSET 172 | pred_id = label % OFFSET 173 | gt_pred_map[(gt_id, pred_id)] = intersection 174 | 175 | # count all matched pairs 176 | gt_matched = set() 177 | pred_matched = set() 178 | for label_tuple, intersection in gt_pred_map.items(): 179 | gt_label, pred_label = label_tuple 180 | if gt_label not in gt_segms: 181 | continue 182 | if pred_label not in pred_segms: 183 | continue 184 | if gt_segms[gt_label]['iscrowd'] == 1: 185 | continue 186 | if gt_segms[gt_label]['category_id'] != pred_segms[pred_label]['category_id']: 187 | continue 188 | 189 | union = pred_segms[pred_label]['area'] + gt_segms[gt_label]['area'] - intersection - gt_pred_map.get((VOID, pred_label), 0) 190 | iou = intersection / union 191 | if iou > 0.5: 192 | pq_stat[gt_segms[gt_label]['category_id']].tp += 1 193 | pq_stat[gt_segms[gt_label]['category_id']].iou += iou 194 | gt_matched.add(gt_label) 195 | pred_matched.add(pred_label) 196 | 197 | # count false positives 198 | crowd_labels_dict = {} 199 | for gt_label, gt_info in gt_segms.items(): 200 | if gt_label in gt_matched: 201 | continue 202 | # crowd segments are ignored 203 | if gt_info['iscrowd'] == 1: 204 | crowd_labels_dict[gt_info['category_id']] = gt_label 205 | continue 206 | pq_stat[gt_info['category_id']].fn += 1 207 | 208 | # count false positives 209 | for pred_label, pred_info in pred_segms.items(): 210 | if pred_label in pred_matched: 211 | continue 212 | # intersection of the segment with VOID 213 | intersection = gt_pred_map.get((VOID, pred_label), 0) 214 | # plus intersection with corresponding CROWD region if it exists 215 | if pred_info['category_id'] in crowd_labels_dict: 216 | intersection += gt_pred_map.get((crowd_labels_dict[pred_info['category_id']], pred_label), 0) 217 | # predicted segment is ignored if more than half of the segment correspond to VOID and CROWD regions 218 | if intersection / pred_info['area'] > 0.5: 219 | continue 220 | pq_stat[pred_info['category_id']].fp += 1 221 | print('Core: {}, all {} images processed'.format(proc_id, len(annotation_set))) 222 | return pq_stat 223 | 224 | 225 | def pq_compute_multi_core(matched_annotations_list, gt_folder, pred_folder, categories): 226 | cpu_num = multiprocessing.cpu_count() 227 | annotations_split = np.array_split(matched_annotations_list, cpu_num) 228 | print("Number of cores: {}, images per core: {}".format(cpu_num, len(annotations_split[0]))) 229 | workers = multiprocessing.Pool(processes=cpu_num) 230 | processes = [] 231 | for proc_id, annotation_set in enumerate(annotations_split): 232 | p = workers.apply_async(pq_compute_single_core, 233 | (proc_id, annotation_set, gt_folder, pred_folder, categories)) 234 | processes.append(p) 235 | pq_stat = PQStat() 236 | for p in processes: 237 | pq_stat += p.get() 238 | return pq_stat 239 | 240 | 241 | def average_pq(pq_stat, categories): 242 | metrics = [("All", None), ("Things", True), ("Stuff", False)] 243 | results = {} 244 | for name, isthing in metrics: 245 | results[name], per_class_results = pq_stat.pq_average(categories, isthing=isthing) 246 | if name == 'All': 247 | results['per_class'] = per_class_results 248 | return results 249 | 250 | 251 | def print_results(results, categories): 252 | metrics = ["All", "Things", "Stuff"] 253 | print("{:14s}| {:>5s} {:>5s} {:>5s}".format("Category", "PQ", "SQ", "RQ")) 254 | labels = sorted(results['per_class'].keys()) 255 | for label in labels: 256 | print("{:14s}| {:5.1f} {:5.1f} {:5.1f}".format( 257 | categories[label]['name'], 258 | 100 * results['per_class'][label]['pq'], 259 | 100 * results['per_class'][label]['sq'], 260 | 100 * results['per_class'][label]['rq'] 261 | )) 262 | print("-" * 41) 263 | print("{:14s}| {:>5s} {:>5s} {:>5s} {:>5s}".format("", "PQ", "SQ", "RQ", "N")) 264 | 265 | for name in metrics: 266 | print("{:14s}| {:5.1f} {:5.1f} {:5.1f} {:5d}".format( 267 | name, 268 | 100 * results[name]['pq'], 269 | 100 * results[name]['sq'], 270 | 100 * results[name]['rq'], 271 | results[name]['n'] 272 | )) 273 | 274 | 275 | def evaluatePanoptic(gt_json_file, gt_folder, pred_json_file, pred_folder, resultsFile): 276 | 277 | start_time = time.time() 278 | with open(gt_json_file, 'r') as f: 279 | gt_json = json.load(f) 280 | with open(pred_json_file, 'r') as f: 281 | pred_json = json.load(f) 282 | categories = {el['id']: el for el in gt_json['categories']} 283 | 284 | print("Evaluation panoptic segmentation metrics:") 285 | print("Ground truth:") 286 | print("\tSegmentation folder: {}".format(gt_folder)) 287 | print("\tJSON file: {}".format(gt_json_file)) 288 | print("Prediction:") 289 | print("\tSegmentation folder: {}".format(pred_folder)) 290 | print("\tJSON file: {}".format(pred_json_file)) 291 | 292 | if not os.path.isdir(gt_folder): 293 | printError("Folder {} with ground truth segmentations doesn't exist".format(gt_folder)) 294 | if not os.path.isdir(pred_folder): 295 | printError("Folder {} with predicted segmentations doesn't exist".format(pred_folder)) 296 | 297 | pred_annotations = {el['image_id']: el for el in pred_json['annotations']} 298 | matched_annotations_list = [] 299 | for gt_ann in gt_json['annotations']: 300 | image_id = gt_ann['image_id'] 301 | if image_id not in pred_annotations: 302 | raise Exception('no prediction for the image with id: {}'.format(image_id)) 303 | matched_annotations_list.append((gt_ann, pred_annotations[image_id])) 304 | 305 | pq_stat = pq_compute_multi_core(matched_annotations_list, gt_folder, pred_folder, categories) 306 | 307 | results = average_pq(pq_stat, categories) 308 | with open(resultsFile, 'w') as f: 309 | print("Saving computed results in {}".format(resultsFile)) 310 | json.dump(results, f, sort_keys=True, indent=4) 311 | print_results(results, categories) 312 | 313 | t_delta = time.time() - start_time 314 | print("Time elapsed: {:0.2f} seconds".format(t_delta)) 315 | 316 | return results 317 | 318 | 319 | # The main method 320 | def main(): 321 | cityscapesPath = os.environ.get( 322 | 'CITYSCAPES_DATASET', os.path.join(os.path.dirname(os.path.realpath(__file__)),'..','..') 323 | ) 324 | gtJsonFile = os.path.join(cityscapesPath, "gtFine", "cityscapes_panoptic_val.json") 325 | 326 | predictionPath = os.environ.get( 327 | 'CITYSCAPES_RESULTS', 328 | os.path.join(cityscapesPath, "results") 329 | ) 330 | predictionJsonFile = os.path.join(predictionPath, "cityscapes_panoptic_val.json") 331 | 332 | parser = argparse.ArgumentParser() 333 | parser.add_argument("--gt-json-file", 334 | dest="gtJsonFile", 335 | help= '''path to json file that contains ground truth in COCO panoptic format. 336 | By default it is $CITYSCAPES_DATASET/gtFine/cityscapes_panoptic_val.json. 337 | ''', 338 | default=gtJsonFile, 339 | type=str) 340 | parser.add_argument("--gt-folder", 341 | dest="gtFolder", 342 | help= '''path to folder that contains ground truth *.png files. If the 343 | argument is not provided this script will look for the *.png files in 344 | 'name' if --gt-json-file set to 'name.json'. 345 | ''', 346 | default=None, 347 | type=str) 348 | parser.add_argument("--prediction-json-file", 349 | dest="predictionJsonFile", 350 | help='''path to json file that contains prediction in COCO panoptic format. 351 | By default is either $CITYSCAPES_RESULTS/cityscapes_panoptic_val.json 352 | or $CITYSCAPES_DATASET/results/cityscapes_panoptic_val.json if 353 | $CITYSCAPES_RESULTS is not set. 354 | ''', 355 | default=predictionJsonFile, 356 | type=str) 357 | parser.add_argument("--prediction-folder", 358 | dest="predictionFolder", 359 | help='''path to folder that contains prediction *.png files. If the 360 | argument is not provided this script will look for the *.png files in 361 | 'name' if --prediction-json-file set to 'name.json'. 362 | ''', 363 | default=None, 364 | type=str) 365 | resultFile = "resultPanopticSemanticLabeling.json" 366 | parser.add_argument("--results_file", 367 | dest="resultsFile", 368 | help="File to store computed panoptic quality. Default: {}".format(resultFile), 369 | default=resultFile, 370 | type=str) 371 | args = parser.parse_args() 372 | 373 | if not os.path.isfile(args.gtJsonFile): 374 | printError("Could not find a ground truth json file in {}. Please run the script with '--help'".format(args.gtJsonFile)) 375 | if args.gtFolder is None: 376 | args.gtFolder = os.path.splitext(args.gtJsonFile)[0] 377 | 378 | if not os.path.isfile(args.predictionJsonFile): 379 | printError("Could not find a prediction json file in {}. Please run the script with '--help'".format(args.predictionJsonFile)) 380 | if args.predictionFolder is None: 381 | args.predictionFolder = os.path.splitext(args.predictionJsonFile)[0] 382 | 383 | evaluatePanoptic(args.gtJsonFile, args.gtFolder, args.predictionJsonFile, args.predictionFolder, args.resultsFile) 384 | 385 | return 386 | 387 | # call the main method 388 | if __name__ == "__main__": 389 | main() 390 | -------------------------------------------------------------------------------- /convert_cityscapes_to_coco.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2017-present, Facebook, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | ############################################################################## 17 | 18 | from __future__ import absolute_import 19 | from __future__ import division 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | 23 | import argparse 24 | import h5py 25 | import json 26 | import os 27 | import scipy.misc 28 | import sys 29 | 30 | import cityscapesscripts.evaluation.instances2dict_with_polygons as cs 31 | import numpy as np 32 | import pycocotools.mask as mask_util 33 | 34 | # Type used for storing masks in polygon format 35 | _POLY_TYPE = list 36 | # Type used for storing masks in RLE format 37 | _RLE_TYPE = dict 38 | 39 | 40 | def is_poly(segm): 41 | """Determine if segm is a polygon. Valid segm expected (polygon or RLE).""" 42 | assert isinstance(segm, (_POLY_TYPE, _RLE_TYPE)), \ 43 | 'Invalid segm type: {}'.format(type(segm)) 44 | return isinstance(segm, _POLY_TYPE) 45 | 46 | 47 | def flip_segms(segms, height, width): 48 | """Left/right flip each mask in a list of masks.""" 49 | def _flip_poly(poly, width): 50 | flipped_poly = np.array(poly) 51 | flipped_poly[0::2] = width - np.array(poly[0::2]) - 1 52 | return flipped_poly.tolist() 53 | 54 | def _flip_rle(rle, height, width): 55 | if 'counts' in rle and type(rle['counts']) == list: 56 | # Magic RLE format handling painfully discovered by looking at the 57 | # COCO API showAnns function. 58 | rle = mask_util.frPyObjects([rle], height, width) 59 | mask = mask_util.decode(rle) 60 | mask = mask[:, ::-1, :] 61 | rle = mask_util.encode(np.array(mask, order='F', dtype=np.uint8)) 62 | return rle 63 | 64 | flipped_segms = [] 65 | for segm in segms: 66 | if is_poly(segm): 67 | # Polygon format 68 | flipped_segms.append([_flip_poly(poly, width) for poly in segm]) 69 | else: 70 | # RLE format 71 | flipped_segms.append(_flip_rle(segm, height, width)) 72 | return flipped_segms 73 | 74 | 75 | def polys_to_mask(polygons, height, width): 76 | """Convert from the COCO polygon segmentation format to a binary mask 77 | encoded as a 2D array of data type numpy.float32. The polygon segmentation 78 | is understood to be enclosed inside a height x width image. The resulting 79 | mask is therefore of shape (height, width). 80 | """ 81 | rle = mask_util.frPyObjects(polygons, height, width) 82 | mask = np.array(mask_util.decode(rle), dtype=np.float32) 83 | # Flatten in case polygons was a list 84 | mask = np.sum(mask, axis=2) 85 | mask = np.array(mask > 0, dtype=np.float32) 86 | return mask 87 | 88 | 89 | def mask_to_bbox(mask): 90 | """Compute the tight bounding box of a binary mask.""" 91 | xs = np.where(np.sum(mask, axis=0) > 0)[0] 92 | ys = np.where(np.sum(mask, axis=1) > 0)[0] 93 | 94 | if len(xs) == 0 or len(ys) == 0: 95 | return None 96 | 97 | x0 = xs[0] 98 | x1 = xs[-1] 99 | y0 = ys[0] 100 | y1 = ys[-1] 101 | return np.array((x0, y0, x1, y1), dtype=np.float32) 102 | 103 | 104 | def polys_to_mask_wrt_box(polygons, box, M): 105 | """Convert from the COCO polygon segmentation format to a binary mask 106 | encoded as a 2D array of data type numpy.float32. The polygon segmentation 107 | is understood to be enclosed in the given box and rasterized to an M x M 108 | mask. The resulting mask is therefore of shape (M, M). 109 | """ 110 | w = box[2] - box[0] 111 | h = box[3] - box[1] 112 | 113 | w = np.maximum(w, 1) 114 | h = np.maximum(h, 1) 115 | 116 | polygons_norm = [] 117 | for poly in polygons: 118 | p = np.array(poly, dtype=np.float32) 119 | p[0::2] = (p[0::2] - box[0]) * M / w 120 | p[1::2] = (p[1::2] - box[1]) * M / h 121 | polygons_norm.append(p) 122 | 123 | rle = mask_util.frPyObjects(polygons_norm, M, M) 124 | mask = np.array(mask_util.decode(rle), dtype=np.float32) 125 | # Flatten in case polygons was a list 126 | mask = np.sum(mask, axis=2) 127 | mask = np.array(mask > 0, dtype=np.float32) 128 | return mask 129 | 130 | 131 | def polys_to_boxes(polys): 132 | """Convert a list of polygons into an array of tight bounding boxes.""" 133 | boxes_from_polys = np.zeros((len(polys), 4), dtype=np.float32) 134 | for i in range(len(polys)): 135 | poly = polys[i] 136 | x0 = min(min(p[::2]) for p in poly) 137 | x1 = max(max(p[::2]) for p in poly) 138 | y0 = min(min(p[1::2]) for p in poly) 139 | y1 = max(max(p[1::2]) for p in poly) 140 | boxes_from_polys[i, :] = [x0, y0, x1, y1] 141 | 142 | return boxes_from_polys 143 | 144 | 145 | def rle_mask_voting( 146 | top_masks, all_masks, all_dets, iou_thresh, binarize_thresh, method='AVG' 147 | ): 148 | """Returns new masks (in correspondence with `top_masks`) by combining 149 | multiple overlapping masks coming from the pool of `all_masks`. Two methods 150 | for combining masks are supported: 'AVG' uses a weighted average of 151 | overlapping mask pixels; 'UNION' takes the union of all mask pixels. 152 | """ 153 | if len(top_masks) == 0: 154 | return 155 | 156 | all_not_crowd = [False] * len(all_masks) 157 | top_to_all_overlaps = mask_util.iou(top_masks, all_masks, all_not_crowd) 158 | decoded_all_masks = [ 159 | np.array(mask_util.decode(rle), dtype=np.float32) for rle in all_masks 160 | ] 161 | decoded_top_masks = [ 162 | np.array(mask_util.decode(rle), dtype=np.float32) for rle in top_masks 163 | ] 164 | all_boxes = all_dets[:, :4].astype(np.int32) 165 | all_scores = all_dets[:, 4] 166 | 167 | # Fill box support with weights 168 | mask_shape = decoded_all_masks[0].shape 169 | mask_weights = np.zeros((len(all_masks), mask_shape[0], mask_shape[1])) 170 | for k in range(len(all_masks)): 171 | ref_box = all_boxes[k] 172 | x_0 = max(ref_box[0], 0) 173 | x_1 = min(ref_box[2] + 1, mask_shape[1]) 174 | y_0 = max(ref_box[1], 0) 175 | y_1 = min(ref_box[3] + 1, mask_shape[0]) 176 | mask_weights[k, y_0:y_1, x_0:x_1] = all_scores[k] 177 | mask_weights = np.maximum(mask_weights, 1e-5) 178 | 179 | top_segms_out = [] 180 | for k in range(len(top_masks)): 181 | # Corner case of empty mask 182 | if decoded_top_masks[k].sum() == 0: 183 | top_segms_out.append(top_masks[k]) 184 | continue 185 | 186 | inds_to_vote = np.where(top_to_all_overlaps[k] >= iou_thresh)[0] 187 | # Only matches itself 188 | if len(inds_to_vote) == 1: 189 | top_segms_out.append(top_masks[k]) 190 | continue 191 | 192 | masks_to_vote = [decoded_all_masks[i] for i in inds_to_vote] 193 | if method == 'AVG': 194 | ws = mask_weights[inds_to_vote] 195 | soft_mask = np.average(masks_to_vote, axis=0, weights=ws) 196 | mask = np.array(soft_mask > binarize_thresh, dtype=np.uint8) 197 | elif method == 'UNION': 198 | # Any pixel that's on joins the mask 199 | soft_mask = np.sum(masks_to_vote, axis=0) 200 | mask = np.array(soft_mask > 1e-5, dtype=np.uint8) 201 | else: 202 | raise NotImplementedError('Method {} is unknown'.format(method)) 203 | rle = mask_util.encode(np.array(mask[:, :, np.newaxis], order='F'))[0] 204 | top_segms_out.append(rle) 205 | 206 | return top_segms_out 207 | 208 | 209 | def rle_mask_nms(masks, dets, thresh, mode='IOU'): 210 | """Performs greedy non-maximum suppression based on an overlap measurement 211 | between masks. The type of measurement is determined by `mode` and can be 212 | either 'IOU' (standard intersection over union) or 'IOMA' (intersection over 213 | mininum area). 214 | """ 215 | if len(masks) == 0: 216 | return [] 217 | if len(masks) == 1: 218 | return [0] 219 | 220 | if mode == 'IOU': 221 | # Computes ious[m1, m2] = area(intersect(m1, m2)) / area(union(m1, m2)) 222 | all_not_crowds = [False] * len(masks) 223 | ious = mask_util.iou(masks, masks, all_not_crowds) 224 | elif mode == 'IOMA': 225 | # Computes ious[m1, m2] = area(intersect(m1, m2)) / min(area(m1), area(m2)) 226 | all_crowds = [True] * len(masks) 227 | # ious[m1, m2] = area(intersect(m1, m2)) / area(m2) 228 | ious = mask_util.iou(masks, masks, all_crowds) 229 | # ... = max(area(intersect(m1, m2)) / area(m2), 230 | # area(intersect(m2, m1)) / area(m1)) 231 | ious = np.maximum(ious, ious.transpose()) 232 | elif mode == 'CONTAINMENT': 233 | # Computes ious[m1, m2] = area(intersect(m1, m2)) / area(m2) 234 | # Which measures how much m2 is contained inside m1 235 | all_crowds = [True] * len(masks) 236 | ious = mask_util.iou(masks, masks, all_crowds) 237 | else: 238 | raise NotImplementedError('Mode {} is unknown'.format(mode)) 239 | 240 | scores = dets[:, 4] 241 | order = np.argsort(-scores) 242 | 243 | keep = [] 244 | while order.size > 0: 245 | i = order[0] 246 | keep.append(i) 247 | ovr = ious[i, order[1:]] 248 | inds_to_keep = np.where(ovr <= thresh)[0] 249 | order = order[inds_to_keep + 1] 250 | 251 | return keep 252 | 253 | 254 | def rle_masks_to_boxes(masks): 255 | """Computes the bounding box of each mask in a list of RLE encoded masks.""" 256 | if len(masks) == 0: 257 | return [] 258 | 259 | decoded_masks = [ 260 | np.array(mask_util.decode(rle), dtype=np.float32) for rle in masks 261 | ] 262 | 263 | def get_bounds(flat_mask): 264 | inds = np.where(flat_mask > 0)[0] 265 | return inds.min(), inds.max() 266 | 267 | boxes = np.zeros((len(decoded_masks), 4)) 268 | keep = [True] * len(decoded_masks) 269 | for i, mask in enumerate(decoded_masks): 270 | if mask.sum() == 0: 271 | keep[i] = False 272 | continue 273 | flat_mask = mask.sum(axis=0) 274 | x0, x1 = get_bounds(flat_mask) 275 | flat_mask = mask.sum(axis=1) 276 | y0, y1 = get_bounds(flat_mask) 277 | boxes[i, :] = (x0, y0, x1, y1) 278 | 279 | return boxes, np.where(keep)[0] 280 | 281 | 282 | def xyxy_to_xywh(xyxy): 283 | """Convert [x1 y1 x2 y2] box format to [x1 y1 w h] format.""" 284 | if isinstance(xyxy, (list, tuple)): 285 | # Single box given as a list of coordinates 286 | assert len(xyxy) == 4 287 | x1, y1 = xyxy[0], xyxy[1] 288 | w = xyxy[2] - x1 + 1 289 | h = xyxy[3] - y1 + 1 290 | return (x1, y1, w, h) 291 | elif isinstance(xyxy, np.ndarray): 292 | # Multiple boxes given as a 2D ndarray 293 | return np.hstack((xyxy[:, 0:2], xyxy[:, 2:4] - xyxy[:, 0:2] + 1)) 294 | else: 295 | raise TypeError('Argument xyxy must be a list, tuple, or numpy array.') 296 | 297 | 298 | def parse_args(): 299 | parser = argparse.ArgumentParser(description='Convert dataset') 300 | parser.add_argument( 301 | '--dataset', help="cocostuff, cityscapes", default='cityscapes_instance_only', type=str) 302 | parser.add_argument( 303 | '--outdir', help="output dir for json files", default='coco_annotations', type=str) 304 | parser.add_argument( 305 | '--datadir', help="data dir for annotations to be converted", 306 | default='./', type=str) 307 | return parser.parse_args() 308 | 309 | 310 | def convert_coco_stuff_mat(data_dir, out_dir): 311 | """Convert to png and save json with path. This currently only contains 312 | the segmentation labels for objects+stuff in cocostuff - if we need to 313 | combine with other labels from original COCO that will be a TODO.""" 314 | sets = ['train', 'val'] 315 | categories = [] 316 | json_name = 'coco_stuff_%s.json' 317 | ann_dict = {} 318 | for data_set in sets: 319 | file_list = os.path.join(data_dir, '%s.txt') 320 | images = [] 321 | with open(file_list % data_set) as f: 322 | for img_id, img_name in enumerate(f): 323 | img_name = img_name.replace('coco', 'COCO').strip('\n') 324 | image = {} 325 | mat_file = os.path.join( 326 | data_dir, 'annotations/%s.mat' % img_name) 327 | data = h5py.File(mat_file, 'r') 328 | labelMap = data.get('S') 329 | if len(categories) == 0: 330 | labelNames = data.get('names') 331 | for idx, n in enumerate(labelNames): 332 | categories.append( 333 | {"id": idx, "name": ''.join(chr(i) for i in data[ 334 | n[0]])}) 335 | ann_dict['categories'] = categories 336 | scipy.misc.imsave( 337 | os.path.join(data_dir, img_name + '.png'), labelMap) 338 | image['width'] = labelMap.shape[0] 339 | image['height'] = labelMap.shape[1] 340 | image['file_name'] = img_name 341 | image['seg_file_name'] = img_name 342 | image['id'] = img_id 343 | images.append(image) 344 | ann_dict['images'] = images 345 | print("Num images: %s" % len(images)) 346 | with open(os.path.join(out_dir, json_name % data_set), 'wb') as outfile: 347 | outfile.write(json.dumps(ann_dict)) 348 | 349 | 350 | # for Cityscapes 351 | def getLabelID(self, instID): 352 | if (instID < 1000): 353 | return instID 354 | else: 355 | return int(instID / 1000) 356 | 357 | 358 | def convert_cityscapes_instance_only( 359 | data_dir, out_dir): 360 | """Convert from cityscapes format to COCO instance seg format - polygons""" 361 | sets = [ 362 | 'gtFine_val', 363 | 'gtFine_train', 364 | 'gtFine_test', 365 | 366 | # 'gtCoarse_train', 367 | # 'gtCoarse_val', 368 | # 'gtCoarse_train_extra' 369 | ] 370 | ann_dirs = [ 371 | 'gtFine/val', 372 | 'gtFine/train', 373 | 'gtFine/test', 374 | 375 | # 'gtCoarse/train', 376 | # 'gtCoarse/train_extra', 377 | # 'gtCoarse/val' 378 | ] 379 | json_name = 'instancesonly_filtered_%s.json' 380 | ends_in = '%s_polygons.json' 381 | img_id = 0 382 | ann_id = 0 383 | cat_id = 1 384 | category_dict = {} 385 | 386 | category_instancesonly = [ 387 | 'person', 388 | 'rider', 389 | 'car', 390 | 'truck', 391 | 'bus', 392 | 'train', 393 | 'motorcycle', 394 | 'bicycle', 395 | ] 396 | 397 | for data_set, ann_dir in zip(sets, ann_dirs): 398 | print('Starting %s' % data_set) 399 | ann_dict = {} 400 | images = [] 401 | annotations = [] 402 | ann_dir = os.path.join(data_dir, ann_dir) 403 | print('anno dir: {}'.format(ann_dir)) 404 | for root, _, files in os.walk(ann_dir): 405 | for filename in files: 406 | if filename.endswith(ends_in % data_set.split('_')[0]): 407 | if len(images) % 50 == 0: 408 | print("Processed %s images, %s annotations" % ( 409 | len(images), len(annotations))) 410 | json_ann = json.load(open(os.path.join(root, filename))) 411 | print('solving: {}'.format(os.path.join(root, filename))) 412 | image = {} 413 | image['id'] = img_id 414 | img_id += 1 415 | 416 | image['width'] = json_ann['imgWidth'] 417 | image['height'] = json_ann['imgHeight'] 418 | image['file_name'] = filename[:-len( 419 | ends_in % data_set.split('_')[0])] + 'leftImg8bit.png' 420 | image['seg_file_name'] = filename[:-len( 421 | ends_in % data_set.split('_')[0])] + \ 422 | '%s_instanceIds.png' % data_set.split('_')[0] 423 | images.append(image) 424 | 425 | fullname = os.path.abspath(os.path.join(root, image['seg_file_name'])) 426 | objects = cs.instances2dict_with_polygons([fullname], verbose=False) 427 | # print(objects) 428 | print(fullname) 429 | objects = objects[fullname] 430 | 431 | for object_cls in objects: 432 | if object_cls not in category_instancesonly: 433 | continue # skip non-instance categories 434 | 435 | for obj in objects[object_cls]: 436 | if obj['contours'] == []: 437 | print('Warning: empty contours.') 438 | continue # skip non-instance categories 439 | 440 | len_p = [len(p) for p in obj['contours']] 441 | if min(len_p) <= 4: 442 | print('Warning: invalid contours.') 443 | continue # skip non-instance categories 444 | 445 | ann = {} 446 | ann['id'] = ann_id 447 | ann_id += 1 448 | ann['image_id'] = image['id'] 449 | ann['segmentation'] = obj['contours'] 450 | 451 | if object_cls not in category_dict: 452 | category_dict[object_cls] = cat_id 453 | cat_id += 1 454 | ann['category_id'] = category_dict[object_cls] 455 | ann['iscrowd'] = 0 456 | ann['area'] = obj['pixelCount'] 457 | ann['bbox'] = xyxy_to_xywh( 458 | polys_to_boxes( 459 | [ann['segmentation']])).tolist()[0] 460 | 461 | annotations.append(ann) 462 | 463 | ann_dict['images'] = images 464 | categories = [{"id": category_dict[name], "name": name} for name in 465 | category_dict] 466 | ann_dict['categories'] = categories 467 | ann_dict['annotations'] = annotations 468 | print("Num categories: %s" % len(categories)) 469 | print("Num images: %s" % len(images)) 470 | print("Num annotations: %s" % len(annotations)) 471 | os.makedirs(out_dir, exist_ok=True) 472 | with open(os.path.join(out_dir, json_name % data_set), 'w') as outfile: 473 | json.dump(ann_dict, outfile) 474 | 475 | 476 | if __name__ == '__main__': 477 | args = parse_args() 478 | if args.dataset == "cityscapes_instance_only": 479 | convert_cityscapes_instance_only(args.datadir, args.outdir) 480 | elif args.dataset == "cocostuff": 481 | convert_coco_stuff_mat(args.datadir, args.outdir) 482 | else: 483 | print("Dataset not supported: %s" % args.dataset) 484 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/evaluation/evalPixelLevelSemanticLabeling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # The evaluation script for pixel-level semantic labeling. 4 | # We use this script to evaluate your approach on the test set. 5 | # You can use the script to evaluate on the validation set. 6 | # 7 | # Please check the description of the "getPrediction" method below 8 | # and set the required environment variables as needed, such that 9 | # this script can locate your results. 10 | # If the default implementation of the method works, then it's most likely 11 | # that our evaluation server will be able to process your results as well. 12 | # 13 | # Note that the script is a faster, if you enable cython support. 14 | # WARNING: Cython only tested for Ubuntu 64bit OS. 15 | # To enable cython, run 16 | # CYTHONIZE_EVAL= python setup.py build_ext --inplace 17 | # 18 | # To run this script, make sure that your results are images, 19 | # where pixels encode the class IDs as defined in labels.py. 20 | # Note that the regular ID is used, not the train ID. 21 | # Further note that many classes are ignored from evaluation. 22 | # Thus, authors are not expected to predict these classes and all 23 | # pixels with a ground truth label that is ignored are ignored in 24 | # evaluation. 25 | 26 | # python imports 27 | from __future__ import print_function, absolute_import, division 28 | import os, sys 29 | import platform 30 | import fnmatch 31 | 32 | try: 33 | from itertools import izip 34 | except ImportError: 35 | izip = zip 36 | 37 | # Cityscapes imports 38 | from cityscapesscripts.helpers.csHelpers import * 39 | 40 | # C Support 41 | # Enable the cython support for faster evaluation 42 | # Only tested for Ubuntu 64bit OS 43 | CSUPPORT = True 44 | # Check if C-Support is available for better performance 45 | if CSUPPORT: 46 | try: 47 | from cityscapesscripts.evaluation import addToConfusionMatrix 48 | except: 49 | CSUPPORT = False 50 | 51 | 52 | ################################### 53 | # PLEASE READ THESE INSTRUCTIONS!!! 54 | ################################### 55 | # Provide the prediction file for the given ground truth file. 56 | # 57 | # The current implementation expects the results to be in a certain root folder. 58 | # This folder is one of the following with decreasing priority: 59 | # - environment variable CITYSCAPES_RESULTS 60 | # - environment variable CITYSCAPES_DATASET/results 61 | # - ../../results/" 62 | # 63 | # Within the root folder, a matching prediction file is recursively searched. 64 | # A file matches, if the filename follows the pattern 65 | # _123456_123456*.png 66 | # for a ground truth filename 67 | # _123456_123456_gtFine_labelIds.png 68 | def getPrediction( args, groundTruthFile ): 69 | # determine the prediction path, if the method is first called 70 | if not args.predictionPath: 71 | rootPath = None 72 | if 'CITYSCAPES_RESULTS' in os.environ: 73 | rootPath = os.environ['CITYSCAPES_RESULTS'] 74 | elif 'CITYSCAPES_DATASET' in os.environ: 75 | rootPath = os.path.join( os.environ['CITYSCAPES_DATASET'] , "results" ) 76 | else: 77 | rootPath = os.path.join(os.path.dirname(os.path.realpath(__file__)),'..','..','results') 78 | 79 | if not os.path.isdir(rootPath): 80 | printError("Could not find a result root folder. Please read the instructions of this method.") 81 | 82 | args.predictionPath = rootPath 83 | 84 | # walk the prediction path, if not happened yet 85 | if not args.predictionWalk: 86 | walk = [] 87 | for root, dirnames, filenames in os.walk(args.predictionPath): 88 | walk.append( (root,filenames) ) 89 | args.predictionWalk = walk 90 | 91 | csFile = getCsFileInfo(groundTruthFile) 92 | filePattern = "{}_{}_{}*.png".format( csFile.city , csFile.sequenceNb , csFile.frameNb ) 93 | 94 | predictionFile = None 95 | for root, filenames in args.predictionWalk: 96 | for filename in fnmatch.filter(filenames, filePattern): 97 | if not predictionFile: 98 | predictionFile = os.path.join(root, filename) 99 | else: 100 | printError("Found multiple predictions for ground truth {}".format(groundTruthFile)) 101 | 102 | if not predictionFile: 103 | printError("Found no prediction for ground truth {}".format(groundTruthFile)) 104 | 105 | return predictionFile 106 | 107 | 108 | ###################### 109 | # Parameters 110 | ###################### 111 | 112 | 113 | # A dummy class to collect all bunch of data 114 | class CArgs(object): 115 | pass 116 | # And a global object of that class 117 | args = CArgs() 118 | 119 | # Where to look for Cityscapes 120 | if 'CITYSCAPES_DATASET' in os.environ: 121 | args.cityscapesPath = os.environ['CITYSCAPES_DATASET'] 122 | else: 123 | args.cityscapesPath = os.path.join(os.path.dirname(os.path.realpath(__file__)),'..','..') 124 | 125 | if 'CITYSCAPES_EXPORT_DIR' in os.environ: 126 | export_dir = os.environ['CITYSCAPES_EXPORT_DIR'] 127 | if not os.path.isdir(export_dir): 128 | raise ValueError("CITYSCAPES_EXPORT_DIR {} is not a directory".format(export_dir)) 129 | args.exportFile = "{}/resultPixelLevelSemanticLabeling.json".format(export_dir) 130 | else: 131 | args.exportFile = os.path.join(args.cityscapesPath, "evaluationResults", "resultPixelLevelSemanticLabeling.json") 132 | # Parameters that should be modified by user 133 | args.groundTruthSearch = os.path.join( args.cityscapesPath , "gtFine" , "val" , "*", "*_gtFine_labelIds.png" ) 134 | 135 | # Remaining params 136 | args.evalInstLevelScore = True 137 | args.evalPixelAccuracy = False 138 | args.evalLabels = [] 139 | args.printRow = 5 140 | args.normalized = True 141 | args.colorized = hasattr(sys.stderr, "isatty") and sys.stderr.isatty() and platform.system()=='Linux' 142 | args.bold = colors.BOLD if args.colorized else "" 143 | args.nocol = colors.ENDC if args.colorized else "" 144 | args.JSONOutput = True 145 | args.quiet = False 146 | 147 | args.avgClassSize = { 148 | "bicycle" : 4672.3249222261 , 149 | "caravan" : 36771.8241758242 , 150 | "motorcycle" : 6298.7200839748 , 151 | "rider" : 3930.4788056518 , 152 | "bus" : 35732.1511111111 , 153 | "train" : 67583.7075812274 , 154 | "car" : 12794.0202738185 , 155 | "person" : 3462.4756337644 , 156 | "truck" : 27855.1264367816 , 157 | "trailer" : 16926.9763313609 , 158 | } 159 | 160 | # store some parameters for finding predictions in the args variable 161 | # the values are filled when the method getPrediction is first called 162 | args.predictionPath = None 163 | args.predictionWalk = None 164 | 165 | 166 | ######################### 167 | # Methods 168 | ######################### 169 | 170 | 171 | # Generate empty confusion matrix and create list of relevant labels 172 | def generateMatrix(args): 173 | args.evalLabels = [] 174 | for label in labels: 175 | if (label.id < 0): 176 | continue 177 | # we append all found labels, regardless of being ignored 178 | args.evalLabels.append(label.id) 179 | maxId = max(args.evalLabels) 180 | # We use longlong type to be sure that there are no overflows 181 | return np.zeros(shape=(maxId+1, maxId+1),dtype=np.ulonglong) 182 | 183 | def generateInstanceStats(args): 184 | instanceStats = {} 185 | instanceStats["classes" ] = {} 186 | instanceStats["categories"] = {} 187 | for label in labels: 188 | if label.hasInstances and not label.ignoreInEval: 189 | instanceStats["classes"][label.name] = {} 190 | instanceStats["classes"][label.name]["tp"] = 0.0 191 | instanceStats["classes"][label.name]["tpWeighted"] = 0.0 192 | instanceStats["classes"][label.name]["fn"] = 0.0 193 | instanceStats["classes"][label.name]["fnWeighted"] = 0.0 194 | for category in category2labels: 195 | labelIds = [] 196 | allInstances = True 197 | for label in category2labels[category]: 198 | if label.id < 0: 199 | continue 200 | if not label.hasInstances: 201 | allInstances = False 202 | break 203 | labelIds.append(label.id) 204 | if not allInstances: 205 | continue 206 | 207 | instanceStats["categories"][category] = {} 208 | instanceStats["categories"][category]["tp"] = 0.0 209 | instanceStats["categories"][category]["tpWeighted"] = 0.0 210 | instanceStats["categories"][category]["fn"] = 0.0 211 | instanceStats["categories"][category]["fnWeighted"] = 0.0 212 | instanceStats["categories"][category]["labelIds"] = labelIds 213 | 214 | return instanceStats 215 | 216 | 217 | # Get absolute or normalized value from field in confusion matrix. 218 | def getMatrixFieldValue(confMatrix, i, j, args): 219 | if args.normalized: 220 | rowSum = confMatrix[i].sum() 221 | if (rowSum == 0): 222 | return float('nan') 223 | return float(confMatrix[i][j]) / rowSum 224 | else: 225 | return confMatrix[i][j] 226 | 227 | # Calculate and return IOU score for a particular label 228 | def getIouScoreForLabel(label, confMatrix, args): 229 | if id2label[label].ignoreInEval: 230 | return float('nan') 231 | 232 | # the number of true positive pixels for this label 233 | # the entry on the diagonal of the confusion matrix 234 | tp = np.longlong(confMatrix[label,label]) 235 | 236 | # the number of false negative pixels for this label 237 | # the row sum of the matching row in the confusion matrix 238 | # minus the diagonal entry 239 | fn = np.longlong(confMatrix[label,:].sum()) - tp 240 | 241 | # the number of false positive pixels for this labels 242 | # Only pixels that are not on a pixel with ground truth label that is ignored 243 | # The column sum of the corresponding column in the confusion matrix 244 | # without the ignored rows and without the actual label of interest 245 | notIgnored = [l for l in args.evalLabels if not id2label[l].ignoreInEval and not l==label] 246 | fp = np.longlong(confMatrix[notIgnored,label].sum()) 247 | 248 | # the denominator of the IOU score 249 | denom = (tp + fp + fn) 250 | if denom == 0: 251 | return float('nan') 252 | 253 | # return IOU 254 | return float(tp) / denom 255 | 256 | # Calculate and return IOU score for a particular label 257 | def getInstanceIouScoreForLabel(label, confMatrix, instStats, args): 258 | if id2label[label].ignoreInEval: 259 | return float('nan') 260 | 261 | labelName = id2label[label].name 262 | if not labelName in instStats["classes"]: 263 | return float('nan') 264 | 265 | tp = instStats["classes"][labelName]["tpWeighted"] 266 | fn = instStats["classes"][labelName]["fnWeighted"] 267 | # false postives computed as above 268 | notIgnored = [l for l in args.evalLabels if not id2label[l].ignoreInEval and not l==label] 269 | fp = np.longlong(confMatrix[notIgnored,label].sum()) 270 | 271 | # the denominator of the IOU score 272 | denom = (tp + fp + fn) 273 | if denom == 0: 274 | return float('nan') 275 | 276 | # return IOU 277 | return float(tp) / denom 278 | 279 | # Calculate prior for a particular class id. 280 | def getPrior(label, confMatrix): 281 | return float(confMatrix[label,:].sum()) / confMatrix.sum() 282 | 283 | # Get average of scores. 284 | # Only computes the average over valid entries. 285 | def getScoreAverage(scoreList, args): 286 | validScores = 0 287 | scoreSum = 0.0 288 | for score in scoreList: 289 | if not math.isnan(scoreList[score]): 290 | validScores += 1 291 | scoreSum += scoreList[score] 292 | if validScores == 0: 293 | return float('nan') 294 | return scoreSum / validScores 295 | 296 | # Calculate and return IOU score for a particular category 297 | def getIouScoreForCategory(category, confMatrix, args): 298 | # All labels in this category 299 | labels = category2labels[category] 300 | # The IDs of all valid labels in this category 301 | labelIds = [label.id for label in labels if not label.ignoreInEval and label.id in args.evalLabels] 302 | # If there are no valid labels, then return NaN 303 | if not labelIds: 304 | return float('nan') 305 | 306 | # the number of true positive pixels for this category 307 | # this is the sum of all entries in the confusion matrix 308 | # where row and column belong to a label ID of this category 309 | tp = np.longlong(confMatrix[labelIds,:][:,labelIds].sum()) 310 | 311 | # the number of false negative pixels for this category 312 | # that is the sum of all rows of labels within this category 313 | # minus the number of true positive pixels 314 | fn = np.longlong(confMatrix[labelIds,:].sum()) - tp 315 | 316 | # the number of false positive pixels for this category 317 | # we count the column sum of all labels within this category 318 | # while skipping the rows of ignored labels and of labels within this category 319 | notIgnoredAndNotInCategory = [l for l in args.evalLabels if not id2label[l].ignoreInEval and id2label[l].category != category] 320 | fp = np.longlong(confMatrix[notIgnoredAndNotInCategory,:][:,labelIds].sum()) 321 | 322 | # the denominator of the IOU score 323 | denom = (tp + fp + fn) 324 | if denom == 0: 325 | return float('nan') 326 | 327 | # return IOU 328 | return float(tp) / denom 329 | 330 | # Calculate and return IOU score for a particular category 331 | def getInstanceIouScoreForCategory(category, confMatrix, instStats, args): 332 | if not category in instStats["categories"]: 333 | return float('nan') 334 | labelIds = instStats["categories"][category]["labelIds"] 335 | 336 | tp = instStats["categories"][category]["tpWeighted"] 337 | fn = instStats["categories"][category]["fnWeighted"] 338 | 339 | # the number of false positive pixels for this category 340 | # same as above 341 | notIgnoredAndNotInCategory = [l for l in args.evalLabels if not id2label[l].ignoreInEval and id2label[l].category != category] 342 | fp = np.longlong(confMatrix[notIgnoredAndNotInCategory,:][:,labelIds].sum()) 343 | 344 | # the denominator of the IOU score 345 | denom = (tp + fp + fn) 346 | if denom == 0: 347 | return float('nan') 348 | 349 | # return IOU 350 | return float(tp) / denom 351 | 352 | 353 | # create a dictionary containing all relevant results 354 | def createResultDict( confMatrix, classScores, classInstScores, categoryScores, categoryInstScores, perImageStats, args ): 355 | # write JSON result file 356 | wholeData = {} 357 | wholeData["confMatrix"] = confMatrix.tolist() 358 | wholeData["priors"] = {} 359 | wholeData["labels"] = {} 360 | for label in args.evalLabels: 361 | wholeData["priors"][id2label[label].name] = getPrior(label, confMatrix) 362 | wholeData["labels"][id2label[label].name] = label 363 | wholeData["classScores"] = classScores 364 | wholeData["classInstScores"] = classInstScores 365 | wholeData["categoryScores"] = categoryScores 366 | wholeData["categoryInstScores"] = categoryInstScores 367 | wholeData["averageScoreClasses"] = getScoreAverage(classScores, args) 368 | wholeData["averageScoreInstClasses"] = getScoreAverage(classInstScores, args) 369 | wholeData["averageScoreCategories"] = getScoreAverage(categoryScores, args) 370 | wholeData["averageScoreInstCategories"] = getScoreAverage(categoryInstScores, args) 371 | 372 | if perImageStats: 373 | wholeData["perImageScores"] = perImageStats 374 | 375 | return wholeData 376 | 377 | def writeJSONFile(wholeData, args): 378 | path = os.path.dirname(args.exportFile) 379 | ensurePath(path) 380 | writeDict2JSON(wholeData, args.exportFile) 381 | 382 | # Print confusion matrix 383 | def printConfMatrix(confMatrix, args): 384 | # print line 385 | print("\b{text:{fill}>{width}}".format(width=15, fill='-', text=" "), end=' ') 386 | for label in args.evalLabels: 387 | print("\b{text:{fill}>{width}}".format(width=args.printRow + 2, fill='-', text=" "), end=' ') 388 | print("\b{text:{fill}>{width}}".format(width=args.printRow + 3, fill='-', text=" ")) 389 | 390 | # print label names 391 | print("\b{text:>{width}} |".format(width=13, text=""), end=' ') 392 | for label in args.evalLabels: 393 | print("\b{text:^{width}} |".format(width=args.printRow, text=id2label[label].name[0]), end=' ') 394 | print("\b{text:>{width}} |".format(width=6, text="Prior")) 395 | 396 | # print line 397 | print("\b{text:{fill}>{width}}".format(width=15, fill='-', text=" "), end=' ') 398 | for label in args.evalLabels: 399 | print("\b{text:{fill}>{width}}".format(width=args.printRow + 2, fill='-', text=" "), end=' ') 400 | print("\b{text:{fill}>{width}}".format(width=args.printRow + 3, fill='-', text=" ")) 401 | 402 | # print matrix 403 | for x in range(0, confMatrix.shape[0]): 404 | if (not x in args.evalLabels): 405 | continue 406 | # get prior of this label 407 | prior = getPrior(x, confMatrix) 408 | # skip if label does not exist in ground truth 409 | if prior < 1e-9: 410 | continue 411 | 412 | # print name 413 | name = id2label[x].name 414 | if len(name) > 13: 415 | name = name[:13] 416 | print("\b{text:>{width}} |".format(width=13,text=name), end=' ') 417 | # print matrix content 418 | for y in range(0, len(confMatrix[x])): 419 | if (not y in args.evalLabels): 420 | continue 421 | matrixFieldValue = getMatrixFieldValue(confMatrix, x, y, args) 422 | print(getColorEntry(matrixFieldValue, args) + "\b{text:>{width}.2f} ".format(width=args.printRow, text=matrixFieldValue) + args.nocol, end=' ') 423 | # print prior 424 | print(getColorEntry(prior, args) + "\b{text:>{width}.4f} ".format(width=6, text=prior) + args.nocol) 425 | # print line 426 | print("\b{text:{fill}>{width}}".format(width=15, fill='-', text=" "), end=' ') 427 | for label in args.evalLabels: 428 | print("\b{text:{fill}>{width}}".format(width=args.printRow + 2, fill='-', text=" "), end=' ') 429 | print("\b{text:{fill}>{width}}".format(width=args.printRow + 3, fill='-', text=" "), end=' ') 430 | 431 | # Print intersection-over-union scores for all classes. 432 | def printClassScores(scoreList, instScoreList, args): 433 | if (args.quiet): 434 | return 435 | print(args.bold + "classes IoU nIoU" + args.nocol) 436 | print("--------------------------------") 437 | for label in args.evalLabels: 438 | if (id2label[label].ignoreInEval): 439 | continue 440 | labelName = str(id2label[label].name) 441 | iouStr = getColorEntry(scoreList[labelName], args) + "{val:>5.3f}".format(val=scoreList[labelName]) + args.nocol 442 | niouStr = getColorEntry(instScoreList[labelName], args) + "{val:>5.3f}".format(val=instScoreList[labelName]) + args.nocol 443 | print("{:<14}: ".format(labelName) + iouStr + " " + niouStr) 444 | 445 | # Print intersection-over-union scores for all categorys. 446 | def printCategoryScores(scoreDict, instScoreDict, args): 447 | if (args.quiet): 448 | return 449 | print(args.bold + "categories IoU nIoU" + args.nocol) 450 | print("--------------------------------") 451 | for categoryName in scoreDict: 452 | if all( label.ignoreInEval for label in category2labels[categoryName] ): 453 | continue 454 | iouStr = getColorEntry(scoreDict[categoryName], args) + "{val:>5.3f}".format(val=scoreDict[categoryName]) + args.nocol 455 | niouStr = getColorEntry(instScoreDict[categoryName], args) + "{val:>5.3f}".format(val=instScoreDict[categoryName]) + args.nocol 456 | print("{:<14}: ".format(categoryName) + iouStr + " " + niouStr) 457 | 458 | # Evaluate image lists pairwise. 459 | def evaluateImgLists(predictionImgList, groundTruthImgList, args): 460 | if len(predictionImgList) != len(groundTruthImgList): 461 | printError("List of images for prediction and groundtruth are not of equal size.") 462 | confMatrix = generateMatrix(args) 463 | instStats = generateInstanceStats(args) 464 | perImageStats = {} 465 | nbPixels = 0 466 | 467 | if not args.quiet: 468 | print("Evaluating {} pairs of images...".format(len(predictionImgList))) 469 | 470 | # Evaluate all pairs of images and save them into a matrix 471 | for i in range(len(predictionImgList)): 472 | predictionImgFileName = predictionImgList[i] 473 | groundTruthImgFileName = groundTruthImgList[i] 474 | #print "Evaluate ", predictionImgFileName, "<>", groundTruthImgFileName 475 | nbPixels += evaluatePair(predictionImgFileName, groundTruthImgFileName, confMatrix, instStats, perImageStats, args) 476 | 477 | # sanity check 478 | if confMatrix.sum() != nbPixels: 479 | printError('Number of analyzed pixels and entries in confusion matrix disagree: contMatrix {}, pixels {}'.format(confMatrix.sum(),nbPixels)) 480 | 481 | if not args.quiet: 482 | print("\rImages Processed: {}".format(i+1), end=' ') 483 | sys.stdout.flush() 484 | if not args.quiet: 485 | print("\n") 486 | 487 | # sanity check 488 | if confMatrix.sum() != nbPixels: 489 | printError('Number of analyzed pixels and entries in confusion matrix disagree: contMatrix {}, pixels {}'.format(confMatrix.sum(),nbPixels)) 490 | 491 | # print confusion matrix 492 | if (not args.quiet): 493 | printConfMatrix(confMatrix, args) 494 | 495 | # Calculate IOU scores on class level from matrix 496 | classScoreList = {} 497 | for label in args.evalLabels: 498 | labelName = id2label[label].name 499 | classScoreList[labelName] = getIouScoreForLabel(label, confMatrix, args) 500 | 501 | # Calculate instance IOU scores on class level from matrix 502 | classInstScoreList = {} 503 | for label in args.evalLabels: 504 | labelName = id2label[label].name 505 | classInstScoreList[labelName] = getInstanceIouScoreForLabel(label, confMatrix, instStats, args) 506 | 507 | # Print IOU scores 508 | if (not args.quiet): 509 | print("") 510 | print("") 511 | printClassScores(classScoreList, classInstScoreList, args) 512 | iouAvgStr = getColorEntry(getScoreAverage(classScoreList, args), args) + "{avg:5.3f}".format(avg=getScoreAverage(classScoreList, args)) + args.nocol 513 | niouAvgStr = getColorEntry(getScoreAverage(classInstScoreList , args), args) + "{avg:5.3f}".format(avg=getScoreAverage(classInstScoreList , args)) + args.nocol 514 | print("--------------------------------") 515 | print("Score Average : " + iouAvgStr + " " + niouAvgStr) 516 | print("--------------------------------") 517 | print("") 518 | 519 | # Calculate IOU scores on category level from matrix 520 | categoryScoreList = {} 521 | for category in category2labels.keys(): 522 | categoryScoreList[category] = getIouScoreForCategory(category,confMatrix,args) 523 | 524 | # Calculate instance IOU scores on category level from matrix 525 | categoryInstScoreList = {} 526 | for category in category2labels.keys(): 527 | categoryInstScoreList[category] = getInstanceIouScoreForCategory(category,confMatrix,instStats,args) 528 | 529 | # Print IOU scores 530 | if (not args.quiet): 531 | print("") 532 | printCategoryScores(categoryScoreList, categoryInstScoreList, args) 533 | iouAvgStr = getColorEntry(getScoreAverage(categoryScoreList, args), args) + "{avg:5.3f}".format(avg=getScoreAverage(categoryScoreList, args)) + args.nocol 534 | niouAvgStr = getColorEntry(getScoreAverage(categoryInstScoreList, args), args) + "{avg:5.3f}".format(avg=getScoreAverage(categoryInstScoreList, args)) + args.nocol 535 | print("--------------------------------") 536 | print("Score Average : " + iouAvgStr + " " + niouAvgStr) 537 | print("--------------------------------") 538 | print("") 539 | 540 | # write result file 541 | allResultsDict = createResultDict( confMatrix, classScoreList, classInstScoreList, categoryScoreList, categoryInstScoreList, perImageStats, args ) 542 | writeJSONFile( allResultsDict, args) 543 | 544 | # return confusion matrix 545 | return allResultsDict 546 | 547 | # Main evaluation method. Evaluates pairs of prediction and ground truth 548 | # images which are passed as arguments. 549 | def evaluatePair(predictionImgFileName, groundTruthImgFileName, confMatrix, instanceStats, perImageStats, args): 550 | # Loading all resources for evaluation. 551 | try: 552 | predictionImg = Image.open(predictionImgFileName) 553 | predictionNp = np.array(predictionImg) 554 | except: 555 | printError("Unable to load " + predictionImgFileName) 556 | try: 557 | groundTruthImg = Image.open(groundTruthImgFileName) 558 | groundTruthNp = np.array(groundTruthImg) 559 | except: 560 | printError("Unable to load " + groundTruthImgFileName) 561 | # load ground truth instances, if needed 562 | if args.evalInstLevelScore: 563 | groundTruthInstanceImgFileName = groundTruthImgFileName.replace("labelIds","instanceIds") 564 | try: 565 | instanceImg = Image.open(groundTruthInstanceImgFileName) 566 | instanceNp = np.array(instanceImg) 567 | except: 568 | printError("Unable to load " + groundTruthInstanceImgFileName) 569 | 570 | # Check for equal image sizes 571 | if (predictionImg.size[0] != groundTruthImg.size[0]): 572 | printError("Image widths of " + predictionImgFileName + " and " + groundTruthImgFileName + " are not equal.") 573 | if (predictionImg.size[1] != groundTruthImg.size[1]): 574 | printError("Image heights of " + predictionImgFileName + " and " + groundTruthImgFileName + " are not equal.") 575 | if ( len(predictionNp.shape) != 2 ): 576 | printError("Predicted image has multiple channels.") 577 | 578 | imgWidth = predictionImg.size[0] 579 | imgHeight = predictionImg.size[1] 580 | nbPixels = imgWidth*imgHeight 581 | 582 | # Evaluate images 583 | if (CSUPPORT): 584 | # using cython 585 | confMatrix = addToConfusionMatrix.cEvaluatePair(predictionNp, groundTruthNp, confMatrix, args.evalLabels) 586 | else: 587 | # the slower python way 588 | encoding_value = max(groundTruthNp.max(), predictionNp.max()).astype(np.int32) + 1 589 | encoded = (groundTruthNp.astype(np.int32) * encoding_value) + predictionNp 590 | 591 | values, cnt = np.unique(encoded, return_counts=True) 592 | 593 | for value, c in zip(values, cnt): 594 | pred_id = value % encoding_value 595 | gt_id = int((value - pred_id)/encoding_value) 596 | if not gt_id in args.evalLabels: 597 | printError("Unknown label with id {:}".format(gt_id)) 598 | confMatrix[gt_id][pred_id] += c 599 | 600 | 601 | if args.evalInstLevelScore: 602 | # Generate category masks 603 | categoryMasks = {} 604 | for category in instanceStats["categories"]: 605 | categoryMasks[category] = np.in1d( predictionNp , instanceStats["categories"][category]["labelIds"] ).reshape(predictionNp.shape) 606 | 607 | instList = np.unique(instanceNp[instanceNp > 1000]) 608 | for instId in instList: 609 | labelId = int(instId/1000) 610 | label = id2label[ labelId ] 611 | if label.ignoreInEval: 612 | continue 613 | 614 | mask = instanceNp==instId 615 | instSize = np.count_nonzero( mask ) 616 | 617 | tp = np.count_nonzero( predictionNp[mask] == labelId ) 618 | fn = instSize - tp 619 | 620 | weight = args.avgClassSize[label.name] / float(instSize) 621 | tpWeighted = float(tp) * weight 622 | fnWeighted = float(fn) * weight 623 | 624 | instanceStats["classes"][label.name]["tp"] += tp 625 | instanceStats["classes"][label.name]["fn"] += fn 626 | instanceStats["classes"][label.name]["tpWeighted"] += tpWeighted 627 | instanceStats["classes"][label.name]["fnWeighted"] += fnWeighted 628 | 629 | category = label.category 630 | if category in instanceStats["categories"]: 631 | catTp = 0 632 | catTp = np.count_nonzero( np.logical_and( mask , categoryMasks[category] ) ) 633 | catFn = instSize - catTp 634 | 635 | catTpWeighted = float(catTp) * weight 636 | catFnWeighted = float(catFn) * weight 637 | 638 | instanceStats["categories"][category]["tp"] += catTp 639 | instanceStats["categories"][category]["fn"] += catFn 640 | instanceStats["categories"][category]["tpWeighted"] += catTpWeighted 641 | instanceStats["categories"][category]["fnWeighted"] += catFnWeighted 642 | 643 | if args.evalPixelAccuracy: 644 | notIgnoredLabels = [l for l in args.evalLabels if not id2label[l].ignoreInEval] 645 | notIgnoredPixels = np.in1d( groundTruthNp , notIgnoredLabels , invert=True ).reshape(groundTruthNp.shape) 646 | erroneousPixels = np.logical_and( notIgnoredPixels , ( predictionNp != groundTruthNp ) ) 647 | perImageStats[predictionImgFileName] = {} 648 | perImageStats[predictionImgFileName]["nbNotIgnoredPixels"] = np.count_nonzero(notIgnoredPixels) 649 | perImageStats[predictionImgFileName]["nbCorrectPixels"] = np.count_nonzero(erroneousPixels) 650 | 651 | return nbPixels 652 | 653 | # The main method 654 | def main(): 655 | global args 656 | argv = sys.argv[1:] 657 | 658 | predictionImgList = [] 659 | groundTruthImgList = [] 660 | 661 | # the image lists can either be provided as arguments 662 | if (len(argv) > 3): 663 | for arg in argv: 664 | if ("gt" in arg or "groundtruth" in arg): 665 | groundTruthImgList.append(arg) 666 | elif ("pred" in arg): 667 | predictionImgList.append(arg) 668 | # however the no-argument way is prefered 669 | elif len(argv) == 0: 670 | # use the ground truth search string specified above 671 | groundTruthImgList = glob.glob(args.groundTruthSearch) 672 | if not groundTruthImgList: 673 | printError("Cannot find any ground truth images to use for evaluation. Searched for: {}".format(args.groundTruthSearch)) 674 | # get the corresponding prediction for each ground truth imag 675 | for gt in groundTruthImgList: 676 | predictionImgList.append( getPrediction(args,gt) ) 677 | 678 | # evaluate 679 | evaluateImgLists(predictionImgList, groundTruthImgList, args) 680 | 681 | return 682 | 683 | # call the main method 684 | if __name__ == "__main__": 685 | main() 686 | -------------------------------------------------------------------------------- /cityscapesScripts/cityscapesscripts/evaluation/evalInstanceLevelSemanticLabeling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # The evaluation script for instance-level semantic labeling. 4 | # We use this script to evaluate your approach on the test set. 5 | # You can use the script to evaluate on the validation set. 6 | # 7 | # Please check the description of the "getPrediction" method below 8 | # and set the required environment variables as needed, such that 9 | # this script can locate your results. 10 | # If the default implementation of the method works, then it's most likely 11 | # that our evaluation server will be able to process your results as well. 12 | # 13 | # To run this script, make sure that your results contain text files 14 | # (one for each test set image) with the content: 15 | # relPathPrediction1 labelIDPrediction1 confidencePrediction1 16 | # relPathPrediction2 labelIDPrediction2 confidencePrediction2 17 | # relPathPrediction3 labelIDPrediction3 confidencePrediction3 18 | # ... 19 | # 20 | # - The given paths "relPathPrediction" point to images that contain 21 | # binary masks for the described predictions, where any non-zero is 22 | # part of the predicted instance. The paths must not contain spaces, 23 | # must be relative to the root directory and must point to locations 24 | # within the root directory. 25 | # - The label IDs "labelIDPrediction" specify the class of that mask, 26 | # encoded as defined in labels.py. Note that the regular ID is used, 27 | # not the train ID. 28 | # - The field "confidencePrediction" is a float value that assigns a 29 | # confidence score to the mask. 30 | # 31 | # Note that this tool creates a file named "gtInstances.json" during its 32 | # first run. This file helps to speed up computation and should be deleted 33 | # whenever anything changes in the ground truth annotations or anything 34 | # goes wrong. 35 | 36 | # python imports 37 | from __future__ import print_function, absolute_import, division 38 | import os, sys 39 | import fnmatch 40 | from copy import deepcopy 41 | 42 | # Cityscapes imports 43 | from cityscapesscripts.helpers.csHelpers import * 44 | from cityscapesscripts.evaluation.instances2dict import instances2dict 45 | 46 | 47 | ################################### 48 | # PLEASE READ THESE INSTRUCTIONS!!! 49 | ################################### 50 | # Provide the prediction file for the given ground truth file. 51 | # Please read the instructions above for a description of 52 | # the result file. 53 | # 54 | # The current implementation expects the results to be in a certain root folder. 55 | # This folder is one of the following with decreasing priority: 56 | # - environment variable CITYSCAPES_RESULTS 57 | # - environment variable CITYSCAPES_DATASET/results 58 | # - ../../results/" 59 | # (Remember to set the variables using "export CITYSCAPES_=".) 60 | # 61 | # Within the root folder, a matching prediction file is recursively searched. 62 | # A file matches, if the filename follows the pattern 63 | # _123456_123456*.txt 64 | # for a ground truth filename 65 | # _123456_123456_gtFine_instanceIds.png 66 | def getPrediction( groundTruthFile , args ): 67 | # determine the prediction path, if the method is first called 68 | if not args.predictionPath: 69 | rootPath = None 70 | if 'CITYSCAPES_RESULTS' in os.environ: 71 | rootPath = os.environ['CITYSCAPES_RESULTS'] 72 | elif 'CITYSCAPES_DATASET' in os.environ: 73 | rootPath = os.path.join( os.environ['CITYSCAPES_DATASET'] , "results" ) 74 | else: 75 | rootPath = os.path.join(os.path.dirname(os.path.realpath(__file__)),'..','..','results') 76 | 77 | if not os.path.isdir(rootPath): 78 | printError("Could not find a result root folder. Please read the instructions of this method.") 79 | 80 | args.predictionPath = os.path.abspath(rootPath) 81 | 82 | # walk the prediction path, if not happened yet 83 | if not args.predictionWalk: 84 | walk = [] 85 | for root, dirnames, filenames in os.walk(args.predictionPath): 86 | walk.append( (root,filenames) ) 87 | args.predictionWalk = walk 88 | 89 | csFile = getCsFileInfo(groundTruthFile) 90 | filePattern = "{}_{}_{}*.txt".format( csFile.city , csFile.sequenceNb , csFile.frameNb ) 91 | 92 | predictionFile = None 93 | for root, filenames in args.predictionWalk: 94 | for filename in fnmatch.filter(filenames, filePattern): 95 | if not predictionFile: 96 | predictionFile = os.path.join(root, filename) 97 | else: 98 | printError("Found multiple predictions for ground truth {}".format(groundTruthFile)) 99 | 100 | if not predictionFile: 101 | printError("Found no prediction for ground truth {}".format(groundTruthFile)) 102 | 103 | return predictionFile 104 | 105 | 106 | ###################### 107 | # Parameters 108 | ###################### 109 | 110 | 111 | # A dummy class to collect all bunch of data 112 | class CArgs(object): 113 | pass 114 | # And a global object of that class 115 | args = CArgs() 116 | 117 | # Where to look for Cityscapes 118 | if 'CITYSCAPES_DATASET' in os.environ: 119 | args.cityscapesPath = os.environ['CITYSCAPES_DATASET'] 120 | else: 121 | args.cityscapesPath = os.path.join(os.path.dirname(os.path.realpath(__file__)),'..','..') 122 | 123 | # Parameters that should be modified by user 124 | args.exportFile = os.path.join( args.cityscapesPath , "evaluationResults" , "resultInstanceLevelSemanticLabeling.json" ) 125 | args.groundTruthSearch = os.path.join( args.cityscapesPath , "gtFine" , "val" , "*", "*_gtFine_instanceIds.png" ) 126 | 127 | # overlaps for evaluation 128 | args.overlaps = np.arange(0.5,1.,0.05) 129 | # minimum region size for evaluation [pixels] 130 | args.minRegionSizes = np.array( [ 100 , 1000 , 1000 ] ) 131 | # distance thresholds [m] 132 | args.distanceThs = np.array( [ float('inf') , 100 , 50 ] ) 133 | # distance confidences 134 | args.distanceConfs = np.array( [ -float('inf') , 0.5 , 0.5 ] ) 135 | 136 | args.gtInstancesFile = os.path.join(os.path.dirname(os.path.realpath(__file__)),'gtInstances.json') 137 | args.distanceAvailable = False 138 | args.JSONOutput = True 139 | args.quiet = False 140 | args.csv = False 141 | args.colorized = True 142 | args.instLabels = [] 143 | 144 | # store some parameters for finding predictions in the args variable 145 | # the values are filled when the method getPrediction is first called 146 | args.predictionPath = None 147 | args.predictionWalk = None 148 | 149 | 150 | # Determine the labels that have instances 151 | def setInstanceLabels(args): 152 | args.instLabels = [] 153 | for label in labels: 154 | if label.hasInstances and not label.ignoreInEval: 155 | args.instLabels.append(label.name) 156 | 157 | # Read prediction info 158 | # imgFile, predId, confidence 159 | def readPredInfo(predInfoFileName,args): 160 | predInfo = {} 161 | if (not os.path.isfile(predInfoFileName)): 162 | printError("Infofile '{}' for the predictions not found.".format(predInfoFileName)) 163 | with open(predInfoFileName, 'r') as f: 164 | for line in f: 165 | splittedLine = line.split(" ") 166 | if len(splittedLine) != 3: 167 | printError( "Invalid prediction file. Expected content: relPathPrediction1 labelIDPrediction1 confidencePrediction1" ) 168 | if os.path.isabs(splittedLine[0]): 169 | printError( "Invalid prediction file. First entry in each line must be a relative path." ) 170 | 171 | filename = os.path.join( os.path.dirname(predInfoFileName),splittedLine[0] ) 172 | filename = os.path.abspath( filename ) 173 | 174 | # check if that file is actually somewhere within the prediction root 175 | if os.path.commonprefix( [filename,args.predictionPath] ) != args.predictionPath: 176 | printError( "Predicted mask {} in prediction text file {} points outside of prediction path.".format(filename,predInfoFileName) ) 177 | 178 | imageInfo = {} 179 | imageInfo["labelID"] = int(float(splittedLine[1])) 180 | imageInfo["conf"] = float(splittedLine[2]) 181 | predInfo[filename] = imageInfo 182 | 183 | return predInfo 184 | 185 | # Routine to read ground truth image 186 | def readGTImage(gtImageFileName,args): 187 | return Image.open(gtImageFileName) 188 | 189 | # either read or compute a dictionary of all ground truth instances 190 | def getGtInstances(groundTruthList,args): 191 | gtInstances = {} 192 | # if there is a global statistics json, then load it 193 | if (os.path.isfile(args.gtInstancesFile)): 194 | if not args.quiet: 195 | print("Loading ground truth instances from JSON.") 196 | with open(args.gtInstancesFile) as json_file: 197 | gtInstances = json.load(json_file) 198 | # otherwise create it 199 | else: 200 | if (not args.quiet): 201 | print("Creating ground truth instances from png files.") 202 | gtInstances = instances2dict(groundTruthList,not args.quiet) 203 | writeDict2JSON(gtInstances, args.gtInstancesFile) 204 | 205 | return gtInstances 206 | 207 | # Filter instances, ignore labels without instances 208 | def filterGtInstances(singleImageInstances,args): 209 | instanceDict = {} 210 | for labelName in singleImageInstances: 211 | if not labelName in args.instLabels: 212 | continue 213 | instanceDict[labelName] = singleImageInstances[labelName] 214 | return instanceDict 215 | 216 | # match ground truth instances with predicted instances 217 | def matchGtWithPreds(predictionList,groundTruthList,gtInstances,args): 218 | matches = {} 219 | if not args.quiet: 220 | print("Matching {} pairs of images...".format(len(predictionList))) 221 | 222 | count = 0 223 | for (pred,gt) in zip(predictionList,groundTruthList): 224 | # key for dicts 225 | dictKey = os.path.abspath(gt) 226 | 227 | # Read input files 228 | gtImage = readGTImage(gt,args) 229 | predInfo = readPredInfo(pred,args) 230 | 231 | # Get and filter ground truth instances 232 | unfilteredInstances = gtInstances[ dictKey ] 233 | curGtInstancesOrig = filterGtInstances(unfilteredInstances,args) 234 | 235 | # Try to assign all predictions 236 | (curGtInstances,curPredInstances) = assignGt2Preds(curGtInstancesOrig, gtImage, predInfo, args) 237 | 238 | # append to global dict 239 | matches[ dictKey ] = {} 240 | matches[ dictKey ]["groundTruth"] = curGtInstances 241 | matches[ dictKey ]["prediction"] = curPredInstances 242 | 243 | count += 1 244 | if not args.quiet: 245 | print("\rImages Processed: {}".format(count), end=' ') 246 | sys.stdout.flush() 247 | 248 | if not args.quiet: 249 | print("") 250 | 251 | return matches 252 | 253 | # For a given frame, assign all predicted instances to ground truth instances 254 | def assignGt2Preds(gtInstancesOrig, gtImage, predInfo, args): 255 | # In this method, we create two lists 256 | # - predInstances: contains all predictions and their associated gt 257 | # - gtInstances: contains all gt instances and their associated predictions 258 | predInstances = {} 259 | predInstCount = 0 260 | 261 | # Create a prediction array for each class 262 | for label in args.instLabels: 263 | predInstances[label] = [] 264 | 265 | # We already know about the gt instances 266 | # Add the matching information array 267 | gtInstances = deepcopy(gtInstancesOrig) 268 | for label in gtInstances: 269 | for gt in gtInstances[label]: 270 | gt["matchedPred"] = [] 271 | 272 | # Make the gt a numpy array 273 | gtNp = np.array(gtImage) 274 | 275 | # Get a mask of void labels in the groundtruth 276 | voidLabelIDList = [] 277 | for label in labels: 278 | if label.ignoreInEval: 279 | voidLabelIDList.append(label.id) 280 | boolVoid = np.in1d(gtNp, voidLabelIDList).reshape(gtNp.shape) 281 | 282 | # Loop through all prediction masks 283 | for predImageFile in predInfo: 284 | # Additional prediction info 285 | labelID = predInfo[predImageFile]["labelID"] 286 | predConf = predInfo[predImageFile]["conf"] 287 | 288 | # label name 289 | labelName = id2label[int(labelID)].name 290 | 291 | # maybe we are not interested in that label 292 | if not labelName in args.instLabels: 293 | continue 294 | 295 | # Read the mask 296 | predImage = Image.open(predImageFile) 297 | predImage = predImage.convert("L") 298 | predNp = np.array(predImage) 299 | 300 | # make the image really binary, i.e. everything non-zero is part of the prediction 301 | boolPredInst = predNp != 0 302 | predPixelCount = np.count_nonzero( boolPredInst ) 303 | 304 | # skip if actually empty 305 | if not predPixelCount: 306 | continue 307 | 308 | # The information we want to collect for this instance 309 | predInstance = {} 310 | predInstance["imgName"] = predImageFile 311 | predInstance["predID"] = predInstCount 312 | predInstance["labelID"] = int(labelID) 313 | predInstance["pixelCount"] = predPixelCount 314 | predInstance["confidence"] = predConf 315 | # Determine the number of pixels overlapping void 316 | predInstance["voidIntersection"] = np.count_nonzero( np.logical_and(boolVoid, boolPredInst) ) 317 | 318 | # A list of all overlapping ground truth instances 319 | matchedGt = [] 320 | 321 | # Loop through all ground truth instances with matching label 322 | # This list contains all ground truth instances that distinguish groups 323 | # We do not know, if a certain instance is actually a single object or a group 324 | # e.g. car or cargroup 325 | # However, for now we treat both the same and do the rest later 326 | for (gtNum,gtInstance) in enumerate(gtInstancesOrig[labelName]): 327 | 328 | intersection = np.count_nonzero( np.logical_and( gtNp == gtInstance["instID"] , boolPredInst) ) 329 | 330 | # If they intersect add them as matches to both dicts 331 | if (intersection > 0): 332 | gtCopy = gtInstance.copy() 333 | predCopy = predInstance.copy() 334 | 335 | # let the two know their intersection 336 | gtCopy["intersection"] = intersection 337 | predCopy["intersection"] = intersection 338 | 339 | # append ground truth to matches 340 | matchedGt.append(gtCopy) 341 | # append prediction to ground truth instance 342 | gtInstances[labelName][gtNum]["matchedPred"].append(predCopy) 343 | 344 | predInstance["matchedGt"] = matchedGt 345 | predInstCount += 1 346 | predInstances[labelName].append(predInstance) 347 | 348 | return (gtInstances,predInstances) 349 | 350 | 351 | def evaluateMatches(matches, args): 352 | # In the end, we need two vectors for each class and for each overlap 353 | # The first vector (y_true) is binary and is 1, where the ground truth says true, 354 | # and is 0 otherwise. 355 | # The second vector (y_score) is float [0...1] and represents the confidence of 356 | # the prediction. 357 | # 358 | # We represent the following cases as: 359 | # | y_true | y_score 360 | # gt instance with matched prediction | 1 | confidence 361 | # gt instance w/o matched prediction | 1 | 0.0 362 | # false positive prediction | 0 | confidence 363 | # 364 | # The current implementation makes only sense for an overlap threshold >= 0.5, 365 | # since only then, a single prediction can either be ignored or matched, but 366 | # never both. Further, it can never match to two gt instances. 367 | # For matching, we vary the overlap and do the following steps: 368 | # 1.) remove all predictions that satisfy the overlap criterion with an ignore region (either void or *group) 369 | # 2.) remove matches that do not satisfy the overlap 370 | # 3.) mark non-matched predictions as false positive 371 | 372 | # AP 373 | overlaps = args.overlaps 374 | # region size 375 | minRegionSizes = args.minRegionSizes 376 | # distance thresholds 377 | distThs = args.distanceThs 378 | # distance confidences 379 | distConfs = args.distanceConfs 380 | # only keep the first, if distances are not available 381 | if not args.distanceAvailable: 382 | minRegionSizes = [ minRegionSizes[0] ] 383 | distThs = [ distThs [0] ] 384 | distConfs = [ distConfs [0] ] 385 | 386 | # last three must be of same size 387 | if len(distThs) != len(minRegionSizes): 388 | printError("Number of distance thresholds and region sizes different") 389 | if len(distThs) != len(distConfs): 390 | printError("Number of distance thresholds and confidences different") 391 | 392 | # Here we hold the results 393 | # First dimension is class, second overlap 394 | ap = np.zeros( (len(distThs) , len(args.instLabels) , len(overlaps)) , np.float ) 395 | 396 | for dI,(minRegionSize,distanceTh,distanceConf) in enumerate(zip(minRegionSizes,distThs,distConfs)): 397 | for (oI,overlapTh) in enumerate(overlaps): 398 | for (lI,labelName) in enumerate(args.instLabels): 399 | y_true = np.empty( 0 ) 400 | y_score = np.empty( 0 ) 401 | # count hard false negatives 402 | hardFns = 0 403 | # found at least one gt and predicted instance? 404 | haveGt = False 405 | havePred = False 406 | 407 | for img in matches: 408 | predInstances = matches[img]["prediction" ][labelName] 409 | gtInstances = matches[img]["groundTruth"][labelName] 410 | # filter groups in ground truth 411 | gtInstances = [ gt for gt in gtInstances if gt["instID"]>=1000 and gt["pixelCount"]>=minRegionSize and gt["medDist"]<=distanceTh and gt["distConf"]>=distanceConf ] 412 | 413 | if gtInstances: 414 | haveGt = True 415 | if predInstances: 416 | havePred = True 417 | 418 | curTrue = np.ones ( len(gtInstances) ) 419 | curScore = np.ones ( len(gtInstances) ) * (-float("inf")) 420 | curMatch = np.zeros( len(gtInstances) , dtype=np.bool ) 421 | 422 | # collect matches 423 | for (gtI,gt) in enumerate(gtInstances): 424 | foundMatch = False 425 | for pred in gt["matchedPred"]: 426 | overlap = float(pred["intersection"]) / (gt["pixelCount"]+pred["pixelCount"]-pred["intersection"]) 427 | if overlap > overlapTh: 428 | # the score 429 | confidence = pred["confidence"] 430 | 431 | # if we already hat a prediction for this groundtruth 432 | # the prediction with the lower score is automatically a false positive 433 | if curMatch[gtI]: 434 | maxScore = max( curScore[gtI] , confidence ) 435 | minScore = min( curScore[gtI] , confidence ) 436 | curScore[gtI] = maxScore 437 | # append false positive 438 | curTrue = np.append(curTrue,0) 439 | curScore = np.append(curScore,minScore) 440 | curMatch = np.append(curMatch,True) 441 | # otherwise set score 442 | else: 443 | foundMatch = True 444 | curMatch[gtI] = True 445 | curScore[gtI] = confidence 446 | 447 | if not foundMatch: 448 | hardFns += 1 449 | 450 | # remove non-matched ground truth instances 451 | curTrue = curTrue [ curMatch==True ] 452 | curScore = curScore[ curMatch==True ] 453 | 454 | # collect non-matched predictions as false positive 455 | for pred in predInstances: 456 | foundGt = False 457 | for gt in pred["matchedGt"]: 458 | overlap = float(gt["intersection"]) / (gt["pixelCount"]+pred["pixelCount"]-gt["intersection"]) 459 | if overlap > overlapTh: 460 | foundGt = True 461 | break 462 | if not foundGt: 463 | # collect number of void and *group pixels 464 | nbIgnorePixels = pred["voidIntersection"] 465 | for gt in pred["matchedGt"]: 466 | # group? 467 | if gt["instID"] < 1000: 468 | nbIgnorePixels += gt["intersection"] 469 | # small ground truth instances 470 | if gt["pixelCount"] < minRegionSize or gt["medDist"]>distanceTh or gt["distConf"]15.3f}".format(apAvg ) + sep 611 | line += getColorEntry(ap50o , args) + sep + "{:>15.3f}".format(ap50o ) + sep 612 | if args.distanceAvailable: 613 | line += getColorEntry(ap50m , args) + sep + "{:>15.3f}".format(ap50m ) + sep 614 | line += getColorEntry(ap100m, args) + sep + "{:>15.3f}".format(ap100m) + sep 615 | line += getColorEntry(ap5050, args) + sep + "{:>15.3f}".format(ap5050) + sep 616 | line += noCol 617 | print(line) 618 | 619 | allApAvg = avgDict["allAp"] 620 | allAp50o = avgDict["allAp50%"] 621 | if args.distanceAvailable: 622 | allAp50m = avgDict["allAp50m"] 623 | allAp100m = avgDict["allAp100m"] 624 | allAp5050 = avgDict["allAp50%50m"] 625 | 626 | if not args.csv: 627 | print("-"*lineLen) 628 | line = "{:<15}".format("average") + sep + col1 629 | line += getColorEntry(allApAvg , args) + sep + "{:>15.3f}".format(allApAvg) + sep 630 | line += getColorEntry(allAp50o , args) + sep + "{:>15.3f}".format(allAp50o) + sep 631 | if args.distanceAvailable: 632 | line += getColorEntry(allAp50m , args) + sep + "{:>15.3f}".format(allAp50m) + sep 633 | line += getColorEntry(allAp100m, args) + sep + "{:>15.3f}".format(allAp100m) + sep 634 | line += getColorEntry(allAp5050, args) + sep + "{:>15.3f}".format(allAp5050) + sep 635 | line += noCol 636 | print(line) 637 | print("") 638 | 639 | def prepareJSONDataForResults(avgDict, aps, args): 640 | JSONData = {} 641 | JSONData["averages"] = avgDict 642 | JSONData["overlaps"] = args.overlaps.tolist() 643 | JSONData["minRegionSizes"] = args.minRegionSizes.tolist() 644 | JSONData["distanceThresholds"] = args.distanceThs.tolist() 645 | JSONData["minStereoDensities"] = args.distanceConfs.tolist() 646 | JSONData["instLabels"] = args.instLabels 647 | JSONData["resultApMatrix"] = aps.tolist() 648 | 649 | return JSONData 650 | 651 | # Work through image list 652 | def evaluateImgLists(predictionList, groundTruthList, args): 653 | # determine labels of interest 654 | setInstanceLabels(args) 655 | # get dictionary of all ground truth instances 656 | gtInstances = getGtInstances(groundTruthList,args) 657 | # match predictions and ground truth 658 | matches = matchGtWithPreds(predictionList,groundTruthList,gtInstances,args) 659 | writeDict2JSON(matches,"matches.json") 660 | # evaluate matches 661 | apScores = evaluateMatches(matches, args) 662 | # averages 663 | avgDict = computeAverages(apScores,args) 664 | # result dict 665 | resDict = prepareJSONDataForResults(avgDict, apScores, args) 666 | if args.JSONOutput: 667 | # create output folder if necessary 668 | path = os.path.dirname(args.exportFile) 669 | ensurePath(path) 670 | # Write APs to JSON 671 | writeDict2JSON(resDict, args.exportFile) 672 | 673 | if not args.quiet: 674 | # Print results 675 | printResults(avgDict, args) 676 | 677 | return resDict 678 | 679 | # The main method 680 | def main(): 681 | global args 682 | argv = sys.argv[1:] 683 | 684 | predictionImgList = [] 685 | groundTruthImgList = [] 686 | 687 | # the image lists can either be provided as arguments 688 | if (len(argv) > 3): 689 | for arg in argv: 690 | if ("gt" in arg or "groundtruth" in arg): 691 | groundTruthImgList.append(arg) 692 | elif ("pred" in arg): 693 | predictionImgList.append(arg) 694 | # however the no-argument way is prefered 695 | elif len(argv) == 0: 696 | # use the ground truth search string specified above 697 | groundTruthImgList = glob.glob(args.groundTruthSearch) 698 | if not groundTruthImgList: 699 | printError("Cannot find any ground truth images to use for evaluation. Searched for: {}".format(args.groundTruthSearch)) 700 | # get the corresponding prediction for each ground truth imag 701 | for gt in groundTruthImgList: 702 | predictionImgList.append( getPrediction(gt,args) ) 703 | 704 | # print some info for user 705 | print("Note that this tool uses the file '{}' to cache the ground truth instances.".format(args.gtInstancesFile)) 706 | print("If anything goes wrong, or if you change the ground truth, please delete the file.") 707 | 708 | # evaluate 709 | evaluateImgLists(predictionImgList, groundTruthImgList, args) 710 | 711 | return 712 | 713 | # call the main method 714 | if __name__ == "__main__": 715 | main() 716 | --------------------------------------------------------------------------------