├── cityscapesscripts ├── __init__.py ├── VERSION ├── helpers │ ├── __init__.py │ ├── version.py │ ├── labels_cityPersons.py │ ├── csHelpers.py │ ├── labels.py │ ├── annotation.py │ └── box3dImageTransform.py ├── viewer │ ├── __init__.py │ └── icons │ │ ├── back.png │ │ ├── disp.png │ │ ├── exit.png │ │ ├── next.png │ │ ├── open.png │ │ ├── play.png │ │ ├── plus.png │ │ ├── zoom.png │ │ ├── help19.png │ │ ├── label.png │ │ ├── minus.png │ │ ├── shuffle.png │ │ └── filepath.png ├── annotation │ ├── __init__.py │ └── icons │ │ ├── back.png │ │ ├── exit.png │ │ ├── minus.png │ │ ├── next.png │ │ ├── open.png │ │ ├── play.png │ │ ├── plus.png │ │ ├── save.png │ │ ├── undo.png │ │ ├── zoom.png │ │ ├── checked6.png │ │ ├── filepath.png │ │ ├── help19.png │ │ ├── layerup.png │ │ ├── modify.png │ │ ├── shuffle.png │ │ ├── highlight.png │ │ ├── layerdown.png │ │ ├── newobject.png │ │ ├── screenshot.png │ │ ├── checked6_red.png │ │ ├── clearpolygon.png │ │ ├── deleteobject.png │ │ └── screenshotToggle.png ├── download │ ├── __init__.py │ └── downloader.py ├── evaluation │ ├── __init__.py │ ├── addToConfusionMatrix_impl.c │ ├── instance.py │ ├── instances2dict.py │ ├── addToConfusionMatrix.pyx │ ├── objectDetectionHelpers.py │ ├── evalPanopticSemanticLabeling.py │ ├── plot3dResults.py │ └── evalPixelLevelSemanticLabeling.py └── preparation │ ├── __init__.py │ ├── createTrainIdLabelImgs.py │ ├── createTrainIdInstanceImgs.py │ ├── json2labelImg.py │ ├── json2instanceImg.py │ └── createPanopticImgs.py ├── setup.cfg ├── docs ├── csCalibration.pdf └── Box3DImageTransform.ipynb ├── .gitignore ├── LICENSE ├── setup.py └── README.md /cityscapesscripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesscripts/VERSION: -------------------------------------------------------------------------------- 1 | 2.2.3 2 | -------------------------------------------------------------------------------- /cityscapesscripts/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesscripts/viewer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesscripts/annotation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesscripts/download/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /cityscapesscripts/evaluation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cityscapesscripts/preparation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /docs/csCalibration.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/docs/csCalibration.pdf -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/back.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/disp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/disp.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/exit.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/next.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/open.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/play.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/plus.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/zoom.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/help19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/help19.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/label.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/minus.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/shuffle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/shuffle.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/back.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/exit.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/minus.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/next.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/open.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/play.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/plus.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/save.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/undo.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/zoom.png -------------------------------------------------------------------------------- /cityscapesscripts/viewer/icons/filepath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/viewer/icons/filepath.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/checked6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/checked6.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/filepath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/filepath.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/help19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/help19.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/layerup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/layerup.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/modify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/modify.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/shuffle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/shuffle.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/highlight.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/layerdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/layerdown.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/newobject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/newobject.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/screenshot.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/checked6_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/checked6_red.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/clearpolygon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/clearpolygon.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/deleteobject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/deleteobject.png -------------------------------------------------------------------------------- /cityscapesscripts/annotation/icons/screenshotToggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serdarch/cityscapesScripts/master/cityscapesscripts/annotation/icons/screenshotToggle.png -------------------------------------------------------------------------------- /cityscapesscripts/helpers/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | with open(os.path.join(os.path.dirname(__file__), '..', 'VERSION')) as f: 6 | version = f.read().strip() 7 | 8 | if __name__ == "__main__": 9 | print(version) 10 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.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 | 66 | # MAC OS 67 | .DS_Store 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [The Cityscapes Authors] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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", 24 | "evaluation", "addToConfusionMatrix.pyx") 25 | ext_modules = cythonize(pyxFile) 26 | 27 | with open("README.md") as f: 28 | readme = f.read() 29 | 30 | with open(os.path.join('cityscapesscripts', 'VERSION')) as f: 31 | version = f.read().strip() 32 | 33 | console_scripts = [ 34 | 'csEvalPixelLevelSemanticLabeling = cityscapesscripts.evaluation.evalPixelLevelSemanticLabeling:main', 35 | 'csEvalInstanceLevelSemanticLabeling = cityscapesscripts.evaluation.evalInstanceLevelSemanticLabeling:main', 36 | 'csEvalPanopticSemanticLabeling = cityscapesscripts.evaluation.evalPanopticSemanticLabeling:main', 37 | 'csEvalObjectDetection3d = cityscapesscripts.evaluation.evalObjectDetection3d:main', 38 | 'csCreateTrainIdLabelImgs = cityscapesscripts.preparation.createTrainIdLabelImgs:main', 39 | 'csCreateTrainIdInstanceImgs = cityscapesscripts.preparation.createTrainIdInstanceImgs:main', 40 | 'csCreatePanopticImgs = cityscapesscripts.preparation.createPanopticImgs:main', 41 | 'csDownload = cityscapesscripts.download.downloader:main', 42 | 'csPlot3dDetectionResults = cityscapesscripts.evaluation.plot3dResults:main' 43 | ] 44 | 45 | gui_scripts = [ 46 | 'csViewer = cityscapesscripts.viewer.cityscapesViewer:main [gui]', 47 | 'csLabelTool = cityscapesscripts.annotation.cityscapesLabelTool:main [gui]' 48 | ] 49 | 50 | config = { 51 | 'name': 'cityscapesScripts', 52 | 'description': 'Scripts for the Cityscapes Dataset', 53 | 'long_description': readme, 54 | 'long_description_content_type': "text/markdown", 55 | 'author': 'Marius Cordts', 56 | 'url': 'https://github.com/mcordts/cityscapesScripts', 57 | 'author_email': 'mail@cityscapes-dataset.net', 58 | 'license': 'https://github.com/mcordts/cityscapesScripts/blob/master/LICENSE', 59 | 'version': version, 60 | 'install_requires': ['numpy', 'matplotlib', 'pillow', 'appdirs', 'pyquaternion', 'coloredlogs', 'tqdm', 'typing', 'requests', 'tqdm'], 61 | 'setup_requires': ['setuptools>=18.0'], 62 | 'extras_require': { 63 | 'gui': ['PyQt5'] 64 | }, 65 | 'packages': find_packages(), 66 | 'scripts': [], 67 | 'entry_points': {'gui_scripts': gui_scripts, 68 | 'console_scripts': console_scripts}, 69 | 'package_data': {'': ['VERSION', 'icons/*.png']}, 70 | 'ext_modules': ext_modules, 71 | 'include_dirs': include_dirs 72 | } 73 | 74 | setup(**config) 75 | -------------------------------------------------------------------------------- /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/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 9 | import sys 10 | import getopt 11 | import glob 12 | import math 13 | import json 14 | from collections import namedtuple 15 | import logging 16 | import traceback 17 | 18 | # Image processing 19 | from PIL import Image 20 | from PIL import ImageDraw 21 | 22 | # Numpy for datastructures 23 | import numpy as np 24 | 25 | # Cityscapes modules 26 | from cityscapesscripts.helpers.annotation import Annotation 27 | from cityscapesscripts.helpers.labels import labels, name2label, id2label, trainId2label, category2labels 28 | 29 | 30 | def printError(message): 31 | """Print an error message and quit""" 32 | print('ERROR: ' + str(message)) 33 | sys.exit(-1) 34 | 35 | 36 | class colors: 37 | """Class for colors""" 38 | RED = '\033[31;1m' 39 | GREEN = '\033[32;1m' 40 | YELLOW = '\033[33;1m' 41 | BLUE = '\033[34;1m' 42 | MAGENTA = '\033[35;1m' 43 | CYAN = '\033[36;1m' 44 | BOLD = '\033[1m' 45 | UNDERLINE = '\033[4m' 46 | ENDC = '\033[0m' 47 | 48 | 49 | def getColorEntry(val, args): 50 | """Colored value output if colorized flag is activated.""" 51 | 52 | if not args.colorized: 53 | return "" 54 | if not isinstance(val, float) or math.isnan(val): 55 | return colors.ENDC 56 | if (val < .20): 57 | return colors.RED 58 | elif (val < .40): 59 | return colors.YELLOW 60 | elif (val < .60): 61 | return colors.BLUE 62 | elif (val < .80): 63 | return colors.CYAN 64 | else: 65 | return colors.GREEN 66 | 67 | 68 | # Cityscapes files have a typical filename structure 69 | # ___[_]. 70 | # This class contains the individual elements as members 71 | # For the sequence and frame number, the strings are returned, including leading zeros 72 | CsFile = namedtuple('csFile', ['city', 'sequenceNb', 'frameNb', 'type', 'type2', 'ext']) 73 | 74 | 75 | def getCsFileInfo(fileName): 76 | """Returns a CsFile object filled from the info in the given filename""" 77 | baseName = os.path.basename(fileName) 78 | parts = baseName.split('_') 79 | parts = parts[:-1] + parts[-1].split('.') 80 | if not parts: 81 | printError('Cannot parse given filename ({}). Does not seem to be a valid Cityscapes file.'.format(fileName)) 82 | if len(parts) == 5: 83 | csFile = CsFile(*parts[:-1], type2="", ext=parts[-1]) 84 | elif len(parts) == 6: 85 | csFile = CsFile(*parts) 86 | else: 87 | printError('Found {} part(s) in given filename ({}). Expected 5 or 6.'.format(len(parts), fileName)) 88 | 89 | return csFile 90 | 91 | 92 | def getCoreImageFileName(filename): 93 | """Returns the part of Cityscapes filenames that is common to all data types 94 | 95 | e.g. for city_123456_123456_gtFine_polygons.json returns city_123456_123456 96 | """ 97 | csFile = getCsFileInfo(filename) 98 | return "{}_{}_{}".format(csFile.city, csFile.sequenceNb, csFile.frameNb) 99 | 100 | 101 | def getDirectory(fileName): 102 | """Returns the directory name for the given filename 103 | 104 | e.g. 105 | fileName = "/foo/bar/foobar.txt" 106 | return value is "bar" 107 | Not much error checking though 108 | """ 109 | dirName = os.path.dirname(fileName) 110 | return os.path.basename(dirName) 111 | 112 | 113 | def ensurePath(path): 114 | """Make sure that the given path exists""" 115 | if not path: 116 | return 117 | if not os.path.isdir(path): 118 | os.makedirs(path) 119 | 120 | 121 | def writeDict2JSON(dictName, fileName): 122 | """Write a dictionary as json file""" 123 | with open(fileName, 'w') as f: 124 | f.write(json.dumps(dictName, default=lambda o: o.__dict__, sort_keys=True, indent=4)) 125 | 126 | 127 | # dummy main 128 | if __name__ == "__main__": 129 | printError("Only for include, not executable on its own.") 130 | -------------------------------------------------------------------------------- /cityscapesscripts/evaluation/objectDetectionHelpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # helper functions for 3D object detection evaluation 4 | # 5 | 6 | import os 7 | import numpy as np 8 | 9 | from typing import List 10 | 11 | # matching methods 12 | MATCHING_AMODAL = 0 13 | MATCHING_MODAL = 1 14 | 15 | 16 | class EvaluationParameters: 17 | """Helper class managing the evaluation parameters 18 | 19 | Attributes: 20 | labels_to_evaluate: list of labels to evaluate 21 | min_iou_to_match: min iou required to accept as TP 22 | max_depth: max depth for evaluation 23 | step_size: step/bin size for DDTP metrics 24 | matching_method: use modal or amodal 2D boxes for matching 25 | cw: working confidence. If set to -1, it will be determined automatically 26 | num_conf: number of different confidence thresholds used for AP calculation 27 | """ 28 | 29 | def __init__( 30 | self, 31 | labels_to_evaluate, # type: List[str] 32 | min_iou_to_match=0.7, # type: float 33 | max_depth=100, # type: int 34 | step_size=5, # type: int 35 | matching_method=MATCHING_AMODAL, # type: int 36 | cw=-1., # type: float 37 | num_conf=50 # type: int 38 | ): 39 | # type: (...) -> None 40 | 41 | self._labels_to_evaluate = labels_to_evaluate 42 | self._min_iou_to_match = min_iou_to_match 43 | self._max_depth = max_depth 44 | self._step_size = step_size 45 | self._matching_method = matching_method 46 | self._cw = cw 47 | self._num_conf = num_conf 48 | 49 | @property 50 | def labels_to_evaluate(self): 51 | return self._labels_to_evaluate 52 | 53 | @property 54 | def min_iou_to_match(self): 55 | return self._min_iou_to_match 56 | 57 | @property 58 | def max_depth(self): 59 | return self._max_depth 60 | 61 | @property 62 | def step_size(self): 63 | return self._step_size 64 | 65 | @property 66 | def matching_method(self): 67 | return self._matching_method 68 | 69 | @property 70 | def cw(self): 71 | return self._cw 72 | 73 | @cw.setter 74 | def cw(self, cw): 75 | self._cw = cw 76 | 77 | @property 78 | def num_conf(self): 79 | return self._num_conf 80 | 81 | 82 | def calcIouMatrix( 83 | gts, # type: np.ndarray, 84 | preds # type: np.ndarray 85 | ): 86 | # type: (...) -> np.ndarray 87 | """Calculates the pairwise Intersection Over Union (IoU) 88 | matrix for a set of GTs and predictions. 89 | 90 | Args: 91 | gts (np.ndarray): GT boxes with shape Mx4 92 | preds (np.ndarray): predictions with shape Nx4 93 | 94 | Returns: 95 | np.ndarray: IoU matrix with shape MxN 96 | """ 97 | xmin_1, ymin_1, xmax_1, ymax_1 = np.split(gts, 4, axis=1) 98 | xmin_2, ymin_2, xmax_2, ymax_2 = np.split(preds, 4, axis=1) 99 | 100 | inter_xmin = np.maximum(xmin_1, np.transpose(xmin_2)) 101 | inter_ymin = np.maximum(ymin_1, np.transpose(ymin_2)) 102 | inter_xmax = np.minimum(xmax_1, np.transpose(xmax_2)) 103 | inter_ymax = np.minimum(ymax_1, np.transpose(ymax_2)) 104 | 105 | inter_area = np.maximum((inter_xmax - inter_xmin + 1), 0) * np.maximum((inter_ymax - inter_ymin + 1), 0) 106 | 107 | area_1 = (xmax_1 - xmin_1 + 1) * (ymax_1 - ymin_1 + 1) 108 | area_2 = (xmax_2 - xmin_2 + 1) * (ymax_2 - ymin_2 + 1) 109 | iou = inter_area / (area_1 + np.transpose(area_2) - inter_area + 1e-10) 110 | 111 | return iou 112 | 113 | 114 | def calcOverlapMatrix( 115 | gt_ignores, # type: np.ndarray, 116 | preds # type: np.ndarray 117 | ): 118 | # type: (...) -> np.ndarray 119 | """Calculates the overlap matrix for a set 120 | of GT ignore regions and predictions. 121 | 122 | Args: 123 | gt_ignores (np.ndarray): GT ignore regions with shape Mx4 124 | preds (np.ndarray): predictions with shape Nx4 125 | 126 | Returns: 127 | np.ndarray: overlap matrix with shape MxN 128 | """ 129 | xmin_1, ymin_1, xmax_1, ymax_1 = np.split(gt_ignores, 4, axis=1) 130 | xmin_2, ymin_2, xmax_2, ymax_2 = np.split(preds, 4, axis=1) 131 | 132 | inter_xmin = np.maximum(xmin_1, np.transpose(xmin_2)) 133 | inter_ymin = np.maximum(ymin_1, np.transpose(ymin_2)) 134 | inter_xmax = np.minimum(xmax_1, np.transpose(xmax_2)) 135 | inter_ymax = np.minimum(ymax_1, np.transpose(ymax_2)) 136 | 137 | inter_area = np.maximum((inter_xmax - inter_xmin + 1), 0) * np.maximum((inter_ymax - inter_ymin + 1), 0) 138 | 139 | area_2 = (xmax_2 - xmin_2 + 1) * (ymax_2 - ymin_2 + 1) 140 | overlap = inter_area / (np.transpose(area_2) + 1e-10) 141 | 142 | return overlap 143 | 144 | 145 | def getFiles( 146 | folder, # type: str 147 | suffix=".json", # type: str 148 | exclude=["results.json"] # type: List[str] 149 | ): 150 | # type: (...) -> List[str] 151 | """Recursively walks through the folder and finds 152 | returns all files that end with ``"suffix"``. 153 | 154 | Args: 155 | folder (str): the directory 156 | suffix (str): the suffix used for filtering 157 | exclude (List[str]): filenames to exclude 158 | 159 | Returns: 160 | List[str]: list of all found files 161 | """ 162 | file_list = [] 163 | for root, _, filenames in os.walk(folder): 164 | for f in filenames: 165 | if f.endswith(suffix) and f not in exclude: 166 | file_list.append(os.path.join(root, f)) 167 | file_list.sort() 168 | 169 | return file_list 170 | -------------------------------------------------------------------------------- /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 | from PIL import Image 24 | from PIL import ImageDraw 25 | 26 | # cityscapes imports 27 | from cityscapesscripts.helpers.annotation import Annotation 28 | from cityscapesscripts.helpers.labels import name2label 29 | 30 | # Print the information 31 | def printHelp(): 32 | print('{} [OPTIONS] inputJson outputImg'.format(os.path.basename(sys.argv[0]))) 33 | print('') 34 | print('Reads labels as polygons in JSON format and converts them to label images,') 35 | print('where each pixel has an ID that represents the ground truth label.') 36 | print('') 37 | print('Options:') 38 | print(' -h Print this help') 39 | print(' -t Use the "trainIDs" instead of the regular mapping. See "labels.py" for details.') 40 | 41 | # Print an error message and quit 42 | def printError(message): 43 | print('ERROR: {}'.format(message)) 44 | print('') 45 | print('USAGE:') 46 | printHelp() 47 | sys.exit(-1) 48 | 49 | # Convert the given annotation to a label image 50 | def createLabelImage(annotation, encoding, outline=None): 51 | # the size of the image 52 | size = ( annotation.imgWidth , annotation.imgHeight ) 53 | 54 | # the background 55 | if encoding == "ids": 56 | background = name2label['unlabeled'].id 57 | elif encoding == "trainIds": 58 | background = name2label['unlabeled'].trainId 59 | elif encoding == "color": 60 | background = name2label['unlabeled'].color 61 | else: 62 | print("Unknown encoding '{}'".format(encoding)) 63 | return None 64 | 65 | # this is the image that we want to create 66 | if encoding == "color": 67 | labelImg = Image.new("RGBA", size, background) 68 | else: 69 | labelImg = Image.new("L", size, background) 70 | 71 | # a drawer to draw into the image 72 | drawer = ImageDraw.Draw( labelImg ) 73 | 74 | # loop over all objects 75 | for obj in annotation.objects: 76 | label = obj.label 77 | polygon = obj.polygon 78 | 79 | # If the object is deleted, skip it 80 | if obj.deleted: 81 | continue 82 | 83 | # If the label is not known, but ends with a 'group' (e.g. cargroup) 84 | # try to remove the s and see if that works 85 | if ( not label in name2label ) and label.endswith('group'): 86 | label = label[:-len('group')] 87 | 88 | if not label in name2label: 89 | printError( "Label '{}' not known.".format(label) ) 90 | 91 | # If the ID is negative that polygon should not be drawn 92 | if name2label[label].id < 0: 93 | continue 94 | 95 | if encoding == "ids": 96 | val = name2label[label].id 97 | elif encoding == "trainIds": 98 | val = name2label[label].trainId 99 | elif encoding == "color": 100 | val = name2label[label].color 101 | 102 | try: 103 | if outline: 104 | drawer.polygon( polygon, fill=val, outline=outline ) 105 | else: 106 | drawer.polygon( polygon, fill=val ) 107 | except: 108 | print("Failed to draw polygon with label {}".format(label)) 109 | raise 110 | 111 | return labelImg 112 | 113 | # A method that does all the work 114 | # inJson is the filename of the json file 115 | # outImg is the filename of the label image that is generated 116 | # encoding can be set to 117 | # - "ids" : classes are encoded using the regular label IDs 118 | # - "trainIds" : classes are encoded using the training IDs 119 | # - "color" : classes are encoded using the corresponding colors 120 | def json2labelImg(inJson,outImg,encoding="ids"): 121 | annotation = Annotation() 122 | annotation.fromJsonFile(inJson) 123 | labelImg = createLabelImage( annotation , encoding ) 124 | labelImg.save( outImg ) 125 | 126 | # The main method, if you execute this script directly 127 | # Reads the command line arguments and calls the method 'json2labelImg' 128 | def main(argv): 129 | trainIds = False 130 | try: 131 | opts, args = getopt.getopt(argv,"ht") 132 | except getopt.GetoptError: 133 | printError( 'Invalid arguments' ) 134 | for opt, arg in opts: 135 | if opt == '-h': 136 | printHelp() 137 | sys.exit(0) 138 | elif opt == '-t': 139 | trainIds = True 140 | else: 141 | printError( "Handling of argument '{}' not implementend".format(opt) ) 142 | 143 | if len(args) == 0: 144 | printError( "Missing input json file" ) 145 | elif len(args) == 1: 146 | printError( "Missing output image filename" ) 147 | elif len(args) > 2: 148 | printError( "Too many arguments" ) 149 | 150 | inJson = args[0] 151 | outImg = args[1] 152 | 153 | if trainIds: 154 | json2labelImg( inJson , outImg , "trainIds" ) 155 | else: 156 | json2labelImg( inJson , outImg ) 157 | 158 | # call the main method 159 | if __name__ == "__main__": 160 | main(sys.argv[1:]) 161 | -------------------------------------------------------------------------------- /cityscapesscripts/download/downloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import print_function, absolute_import, division, unicode_literals 4 | 5 | import appdirs 6 | import argparse 7 | import getpass 8 | import hashlib 9 | import json 10 | import os 11 | import requests 12 | import shutil 13 | import stat 14 | from tqdm import tqdm 15 | 16 | from builtins import input 17 | 18 | 19 | def login(): 20 | appname = __name__.split('.')[0] 21 | appauthor = 'cityscapes' 22 | data_dir = appdirs.user_data_dir(appname, appauthor) 23 | credentials_file = os.path.join(data_dir, 'credentials.json') 24 | 25 | if os.path.isfile(credentials_file): 26 | with open(credentials_file, 'r') as f: 27 | credentials = json.load(f) 28 | else: 29 | username = input("Cityscapes username or email address: ") 30 | password = getpass.getpass("Cityscapes password: ") 31 | 32 | credentials = { 33 | 'username': username, 34 | 'password': password 35 | } 36 | 37 | store_question = "Store credentials unencrypted in '{}' [y/N]: " 38 | store_question = store_question.format(credentials_file) 39 | store = input(store_question).strip().lower() 40 | if store in ['y', 'yes']: 41 | os.makedirs(data_dir, exist_ok=True) 42 | with open(credentials_file, 'w') as f: 43 | json.dump(credentials, f) 44 | os.chmod(credentials_file, stat.S_IREAD | stat.S_IWRITE) 45 | 46 | session = requests.Session() 47 | r = session.get("https://www.cityscapes-dataset.com/login", 48 | allow_redirects=False) 49 | r.raise_for_status() 50 | credentials['submit'] = 'Login' 51 | r = session.post("https://www.cityscapes-dataset.com/login", 52 | data=credentials, allow_redirects=False) 53 | r.raise_for_status() 54 | 55 | # login was successful, if user is redirected 56 | if r.status_code != 302: 57 | if os.path.isfile(credentials_file): 58 | os.remove(credentials_file) 59 | raise Exception("Bad credentials.") 60 | 61 | return session 62 | 63 | 64 | def get_available_packages(*, session): 65 | r = session.get( 66 | "https://www.cityscapes-dataset.com/downloads/?list", allow_redirects=False) 67 | r.raise_for_status() 68 | return r.json() 69 | 70 | 71 | def list_available_packages(*, session): 72 | packages = get_available_packages(session=session) 73 | print("The following packages are available for download.") 74 | print("Please refer to https://www.cityscapes-dataset.com/downloads/ " 75 | "for additional packages and instructions on properly citing third party packages.") 76 | for p in packages: 77 | info = ' {} -> {}'.format(p['name'], p['size']) 78 | if p['thirdparty'] == '1': 79 | info += " (third party)" 80 | print(info) 81 | 82 | 83 | def parse_size_to_bytes(size_str): 84 | size_str = size_str.upper() 85 | if size_str.endswith("KB"): 86 | size_bytes = float(size_str[:-2]) * 1024 87 | elif size_str.endswith("MB"): 88 | size_bytes = float(size_str[:-2]) * 1024 * 1024 89 | elif size_str.endswith("GB"): 90 | size_bytes = float(size_str[:-2]) * 1024 * 1024 * 1024 91 | else: 92 | raise ValueError("Invalid size format. Use 'KB', 'MB', or 'GB'.") 93 | 94 | return size_bytes 95 | 96 | 97 | def download_packages(*, session, package_names, destination_path, resume=False): 98 | if not os.path.isdir(destination_path): 99 | raise Exception( 100 | "Destination path '{}' does not exist.".format(destination_path)) 101 | 102 | packages = get_available_packages(session=session) 103 | name_to_id = {p['name']: p['packageID'] for p in packages} 104 | name_to_bytes = {p['name']: parse_size_to_bytes(p['size']) for p in packages} 105 | invalid_names = [n for n in package_names if n not in name_to_id] 106 | if invalid_names: 107 | raise Exception( 108 | "These packages do not exist or you don't have access: {}".format(invalid_names)) 109 | 110 | for package_name in package_names: 111 | local_filename = os.path.join(destination_path, package_name) 112 | package_id = name_to_id[package_name] 113 | 114 | print("Downloading cityscapes package '{}' to '{}'".format( 115 | package_name, local_filename)) 116 | 117 | if os.path.exists(local_filename): 118 | if resume: 119 | print("Resuming previous download") 120 | else: 121 | raise Exception( 122 | "Destination file '{}' already exists.".format(local_filename)) 123 | 124 | # md5sum 125 | url = "https://www.cityscapes-dataset.com/md5-sum/?packageID={}".format( 126 | package_id) 127 | r = session.get(url, allow_redirects=False) 128 | r.raise_for_status() 129 | md5sum = r.text.split()[0] 130 | 131 | # download in chunks, support resume 132 | url = "https://www.cityscapes-dataset.com/file-handling/?packageID={}".format( 133 | package_id) 134 | with open(local_filename, 'ab' if resume else 'wb') as f: 135 | resume_header = { 136 | 'Range': 'bytes={}-'.format(f.tell())} if resume else {} 137 | with session.get(url, allow_redirects=False, stream=True, headers=resume_header) as r: 138 | r.raise_for_status() 139 | assert r.status_code in [200, 206] 140 | 141 | # progress bar 142 | with tqdm(desc="Download progress", 143 | total=name_to_bytes[package_name], 144 | miniters=1, 145 | unit='B', 146 | unit_scale=True, 147 | unit_divisor=1024, 148 | initial=f.tell() if resume else 0 149 | ) as pbar: 150 | for chunk in r.iter_content(chunk_size=1024): 151 | f.write(chunk) 152 | pbar.update(len(chunk)) 153 | 154 | # verify md5sum 155 | hash_md5 = hashlib.md5() 156 | with open(local_filename, "rb") as f: 157 | for chunk in iter(lambda: f.read(4096), b""): 158 | hash_md5.update(chunk) 159 | if md5sum != hash_md5.hexdigest(): 160 | raise Exception("MD5 sum of downloaded file does not match.") 161 | 162 | 163 | def parse_arguments(): 164 | description = "Download packages of the Cityscapes Dataset." 165 | epilog = "Requires an account that can be created via https://www.cityscapes-dataset.com/register/" 166 | parser = argparse.ArgumentParser(description=description, epilog=epilog) 167 | 168 | parser.add_argument('-l', '--list_available', action='store_true', 169 | help="list available packages and exit") 170 | 171 | parser.add_argument('-d', '--destination_path', default='.', 172 | help="destination path for downloads") 173 | 174 | parser.add_argument('-r', '--resume', action='store_true', 175 | help="resume previous download") 176 | 177 | parser.add_argument('package_name', nargs='*', 178 | help="name of the packages to download") 179 | 180 | return parser.parse_args() 181 | 182 | 183 | def main(): 184 | args = parse_arguments() 185 | 186 | session = login() 187 | 188 | if args.list_available: 189 | list_available_packages(session=session) 190 | return 191 | 192 | download_packages(session=session, package_names=args.package_name, 193 | destination_path=args.destination_path, 194 | resume=args.resume) 195 | 196 | 197 | if __name__ == "__main__": 198 | main() 199 | -------------------------------------------------------------------------------- /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 | from PIL import Image 41 | from PIL import ImageDraw 42 | 43 | # cityscapes imports 44 | from cityscapesscripts.helpers.annotation import Annotation 45 | from cityscapesscripts.helpers.labels import labels, name2label 46 | 47 | # Print the information 48 | def printHelp(): 49 | print('{} [OPTIONS] inputJson outputImg'.format(os.path.basename(sys.argv[0]))) 50 | print('') 51 | print(' Reads labels as polygons in JSON format and converts them to instance images,') 52 | print(' where each pixel has an ID that represents the ground truth class and the') 53 | print(' individual instance of that class.') 54 | print('') 55 | print(' The pixel values encode both, class and the individual instance.') 56 | print(' The integer part of a division by 1000 of each ID provides the class ID,') 57 | print(' as described in labels.py. The remainder is the instance ID. If a certain') 58 | print(' annotation describes multiple instances, then the pixels have the regular') 59 | print(' ID of that class.') 60 | print('') 61 | print(' Example:') 62 | print(' Let\'s say your labels.py assigns the ID 26 to the class "car".') 63 | print(' Then, the individual cars in an image get the IDs 26000, 26001, 26002, ... .') 64 | print(' A group of cars, where our annotators could not identify the individual') 65 | print(' instances anymore, is assigned to the ID 26.') 66 | print('') 67 | print(' Note that not all classes distinguish instances (see labels.py for a full list).') 68 | print(' The classes without instance annotations are always directly encoded with') 69 | print(' their regular ID, e.g. 11 for "building".') 70 | print('') 71 | print('Options:') 72 | print(' -h Print this help') 73 | print(' -t Use the "trainIDs" instead of the regular mapping. See "labels.py" for details.') 74 | 75 | # Print an error message and quit 76 | def printError(message): 77 | print('ERROR: {}'.format(message)) 78 | print('') 79 | print('USAGE:') 80 | printHelp() 81 | sys.exit(-1) 82 | 83 | # Convert the given annotation to a label image 84 | def createInstanceImage(annotation, encoding): 85 | # the size of the image 86 | size = ( annotation.imgWidth , annotation.imgHeight ) 87 | 88 | # the background 89 | if encoding == "ids": 90 | backgroundId = name2label['unlabeled'].id 91 | elif encoding == "trainIds": 92 | backgroundId = name2label['unlabeled'].trainId 93 | else: 94 | print("Unknown encoding '{}'".format(encoding)) 95 | return None 96 | 97 | # this is the image that we want to create 98 | instanceImg = Image.new("I", size, backgroundId) 99 | 100 | # a drawer to draw into the image 101 | drawer = ImageDraw.Draw( instanceImg ) 102 | 103 | # a dict where we keep track of the number of instances that 104 | # we already saw of each class 105 | nbInstances = {} 106 | for labelTuple in labels: 107 | if labelTuple.hasInstances: 108 | nbInstances[labelTuple.name] = 0 109 | 110 | # loop over all objects 111 | for obj in annotation.objects: 112 | label = obj.label 113 | polygon = obj.polygon 114 | 115 | # If the object is deleted, skip it 116 | if obj.deleted: 117 | continue 118 | 119 | # if the label is not known, but ends with a 'group' (e.g. cargroup) 120 | # try to remove the s and see if that works 121 | # also we know that this polygon describes a group 122 | isGroup = False 123 | if ( not label in name2label ) and label.endswith('group'): 124 | label = label[:-len('group')] 125 | isGroup = True 126 | 127 | if not label in name2label: 128 | printError( "Label '{}' not known.".format(label) ) 129 | 130 | # the label tuple 131 | labelTuple = name2label[label] 132 | 133 | # get the class ID 134 | if encoding == "ids": 135 | id = labelTuple.id 136 | elif encoding == "trainIds": 137 | id = labelTuple.trainId 138 | 139 | # if this label distinguishs between individual instances, 140 | # make the id a instance ID 141 | if labelTuple.hasInstances and not isGroup and id != 255: 142 | id = id * 1000 + nbInstances[label] 143 | nbInstances[label] += 1 144 | 145 | # If the ID is negative that polygon should not be drawn 146 | if id < 0: 147 | continue 148 | 149 | try: 150 | drawer.polygon( polygon, fill=id ) 151 | except: 152 | print("Failed to draw polygon with label {} and id {}: {}".format(label,id,polygon)) 153 | raise 154 | 155 | return instanceImg 156 | 157 | # A method that does all the work 158 | # inJson is the filename of the json file 159 | # outImg is the filename of the instance image that is generated 160 | # encoding can be set to 161 | # - "ids" : classes are encoded using the regular label IDs 162 | # - "trainIds" : classes are encoded using the training IDs 163 | def json2instanceImg(inJson,outImg,encoding="ids"): 164 | annotation = Annotation() 165 | annotation.fromJsonFile(inJson) 166 | instanceImg = createInstanceImage( annotation , encoding ) 167 | instanceImg.save( outImg ) 168 | 169 | # The main method, if you execute this script directly 170 | # Reads the command line arguments and calls the method 'json2instanceImg' 171 | def main(argv): 172 | trainIds = False 173 | try: 174 | opts, args = getopt.getopt(argv,"ht") 175 | except getopt.GetoptError: 176 | printError( 'Invalid arguments' ) 177 | for opt, arg in opts: 178 | if opt == '-h': 179 | printHelp() 180 | sys.exit(0) 181 | elif opt == '-t': 182 | trainIds = True 183 | else: 184 | printError( "Handling of argument '{}' not implementend".format(opt) ) 185 | 186 | if len(args) == 0: 187 | printError( "Missing input json file" ) 188 | elif len(args) == 1: 189 | printError( "Missing output image filename" ) 190 | elif len(args) > 2: 191 | printError( "Too many arguments" ) 192 | 193 | inJson = args[0] 194 | outImg = args[1] 195 | 196 | if trainIds: 197 | json2instanceImg( inJson , outImg , 'trainIds' ) 198 | else: 199 | json2instanceImg( inJson , outImg ) 200 | 201 | # call the main method 202 | if __name__ == "__main__": 203 | main(sys.argv[1:]) 204 | -------------------------------------------------------------------------------- /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 | from PIL import Image 27 | 28 | # cityscapes imports 29 | from cityscapesscripts.helpers.csHelpers import printError 30 | from cityscapesscripts.helpers.labels import id2label, labels 31 | 32 | 33 | # The main method 34 | def convert2panoptic(cityscapesPath=None, outputFolder=None, useTrainId=False, setNames=["val", "train", "test"]): 35 | # Where to look for Cityscapes 36 | if cityscapesPath is None: 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 | cityscapesPath = os.path.join(cityscapesPath, "gtFine") 42 | 43 | if outputFolder is None: 44 | outputFolder = cityscapesPath 45 | 46 | categories = [] 47 | for label in labels: 48 | if label.ignoreInEval: 49 | continue 50 | categories.append({'id': int(label.trainId) if useTrainId else int(label.id), 51 | 'name': label.name, 52 | 'color': label.color, 53 | 'supercategory': label.category, 54 | 'isthing': 1 if label.hasInstances else 0}) 55 | 56 | for setName in setNames: 57 | # how to search for all ground truth 58 | searchFine = os.path.join(cityscapesPath, setName, "*", "*_instanceIds.png") 59 | # search files 60 | filesFine = glob.glob(searchFine) 61 | filesFine.sort() 62 | 63 | files = filesFine 64 | # quit if we did not find anything 65 | if not files: 66 | printError( 67 | "Did not find any files for {} set using matching pattern {}. Please consult the README.".format(setName, searchFine) 68 | ) 69 | # a bit verbose 70 | print("Converting {} annotation files for {} set.".format(len(files), setName)) 71 | 72 | trainIfSuffix = "_trainId" if useTrainId else "" 73 | outputBaseFile = "cityscapes_panoptic_{}{}".format(setName, trainIfSuffix) 74 | outFile = os.path.join(outputFolder, "{}.json".format(outputBaseFile)) 75 | print("Json file with the annotations in panoptic format will be saved in {}".format(outFile)) 76 | panopticFolder = os.path.join(outputFolder, outputBaseFile) 77 | if not os.path.isdir(panopticFolder): 78 | print("Creating folder {} for panoptic segmentation PNGs".format(panopticFolder)) 79 | os.mkdir(panopticFolder) 80 | print("Corresponding segmentations in .png format will be saved in {}".format(panopticFolder)) 81 | 82 | images = [] 83 | annotations = [] 84 | for progress, f in enumerate(files): 85 | 86 | originalFormat = np.array(Image.open(f)) 87 | 88 | fileName = os.path.basename(f) 89 | imageId = fileName.replace("_gtFine_instanceIds.png", "") 90 | inputFileName = fileName.replace("_instanceIds.png", "_leftImg8bit.png") 91 | outputFileName = fileName.replace("_instanceIds.png", "_panoptic.png") 92 | # image entry, id for image is its filename without extension 93 | images.append({"id": imageId, 94 | "width": int(originalFormat.shape[1]), 95 | "height": int(originalFormat.shape[0]), 96 | "file_name": inputFileName}) 97 | 98 | pan_format = np.zeros( 99 | (originalFormat.shape[0], originalFormat.shape[1], 3), dtype=np.uint8 100 | ) 101 | 102 | segmentIds = np.unique(originalFormat) 103 | segmInfo = [] 104 | for segmentId in segmentIds: 105 | if segmentId < 1000: 106 | semanticId = segmentId 107 | isCrowd = 1 108 | else: 109 | semanticId = segmentId // 1000 110 | isCrowd = 0 111 | labelInfo = id2label[semanticId] 112 | categoryId = labelInfo.trainId if useTrainId else labelInfo.id 113 | if labelInfo.ignoreInEval: 114 | continue 115 | if not labelInfo.hasInstances: 116 | isCrowd = 0 117 | 118 | mask = originalFormat == segmentId 119 | color = [segmentId % 256, segmentId // 256, segmentId // 256 // 256] 120 | pan_format[mask] = color 121 | 122 | area = np.sum(mask) # segment area computation 123 | 124 | # bbox computation for a segment 125 | hor = np.sum(mask, axis=0) 126 | hor_idx = np.nonzero(hor)[0] 127 | x = hor_idx[0] 128 | width = hor_idx[-1] - x + 1 129 | vert = np.sum(mask, axis=1) 130 | vert_idx = np.nonzero(vert)[0] 131 | y = vert_idx[0] 132 | height = vert_idx[-1] - y + 1 133 | bbox = [int(x), int(y), int(width), int(height)] 134 | 135 | segmInfo.append({"id": int(segmentId), 136 | "category_id": int(categoryId), 137 | "area": int(area), 138 | "bbox": bbox, 139 | "iscrowd": isCrowd}) 140 | 141 | annotations.append({'image_id': imageId, 142 | 'file_name': outputFileName, 143 | "segments_info": segmInfo}) 144 | 145 | Image.fromarray(pan_format).save(os.path.join(panopticFolder, outputFileName)) 146 | 147 | print("\rProgress: {:>3.2f} %".format((progress + 1) * 100 / len(files)), end=' ') 148 | sys.stdout.flush() 149 | 150 | print("\nSaving the json file {}".format(outFile)) 151 | d = {'images': images, 152 | 'annotations': annotations, 153 | 'categories': categories} 154 | with open(outFile, 'w') as f: 155 | json.dump(d, f, sort_keys=True, indent=4) 156 | 157 | 158 | def main(): 159 | parser = argparse.ArgumentParser() 160 | parser.add_argument("--dataset-folder", 161 | dest="cityscapesPath", 162 | help="path to the Cityscapes dataset 'gtFine' folder", 163 | default=None, 164 | type=str) 165 | parser.add_argument("--output-folder", 166 | dest="outputFolder", 167 | help="path to the output folder.", 168 | default=None, 169 | type=str) 170 | parser.add_argument("--use-train-id", action="store_true", dest="useTrainId") 171 | parser.add_argument("--set-names", 172 | dest="setNames", 173 | help="set names to which apply the function to", 174 | nargs='+', 175 | default=["val", "train", "test"], 176 | type=str) 177 | args = parser.parse_args() 178 | 179 | convert2panoptic(args.cityscapesPath, args.outputFolder, args.useTrainId, args.setNames) 180 | 181 | 182 | # call the main 183 | if __name__ == "__main__": 184 | main() 185 | -------------------------------------------------------------------------------- /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.com 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 | - `gtBbox3d` 3D bounding box annotations of vehicles. Please refer to [Cityscapes 3D (Gählert et al., CVPRW '20)](https://arxiv.org/abs/2006.07864) for details. 28 | - `gtBboxCityPersons` pedestrian bounding box annotations, available for all training and validation images. Please refer to `helpers/labels_cityPersons.py` as well as [CityPersons (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. 29 | - `leftImg8bit` the left images in 8-bit LDR format. These are the standard annotated images. 30 | - `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. 31 | - `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. 32 | - `rightImg8bit` the right stereo views in 8-bit LDR format. 33 | - `rightImg16bit` the right stereo views in 16-bit HDR format. 34 | - `timestamp` the time of recording in ns. The first frame of each sequence always has a timestamp of 0. 35 | - `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. 36 | - `camera` internal and external camera calibration. For details, please refer to [csCalibration.pdf](docs/csCalibration.pdf) 37 | - `vehicle` vehicle odometry, GPS coordinates, and outside temperature. For details, please refer to [csCalibration.pdf](docs/csCalibration.pdf) 38 | 39 | 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. 40 | 41 | Possible values of `split` 42 | - `train` usually used for training, contains 2975 images with fine and coarse annotations 43 | - `val` should be used for validation of hyper-parameters, contains 500 image with fine and coarse annotations. Can also be used for training. 44 | - `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. 45 | - `train_extra` can be optionally used for training, contains 19998 images with coarse annotations 46 | - `demoVideo` video sequences that could be used for qualitative evaluation, no annotations are available for these videos 47 | 48 | 49 | ## Scripts 50 | 51 | ### Installation 52 | 53 | Install `cityscapesscripts` with `pip` 54 | ``` 55 | python -m pip install cityscapesscripts 56 | ``` 57 | 58 | Graphical tools (viewer and label tool) are based on Qt5 and can be installed via 59 | ``` 60 | python -m pip install cityscapesscripts[gui] 61 | ``` 62 | 63 | ### Usage 64 | 65 | The installation installs the cityscapes scripts as a python module named `cityscapesscripts` and exposes the following tools 66 | - `csDownload`: Download the cityscapes packages via command line. 67 | - `csViewer`: View the images and overlay the annotations. 68 | - `csLabelTool`: Tool that we used for labeling. 69 | - `csEvalPixelLevelSemanticLabeling`: Evaluate pixel-level semantic labeling results on the validation set. This tool is also used to evaluate the results on the test set. 70 | - `csEvalInstanceLevelSemanticLabeling`: Evaluate instance-level semantic labeling results on the validation set. This tool is also used to evaluate the results on the test set. 71 | - `csEvalPanopticSemanticLabeling`: Evaluate panoptic segmentation results on the validation set. This tool is also used to evaluate the results on the test set. 72 | - `csEvalObjectDetection3d`: Evaluate 3D object detection on the validation set. This tool is also used to evaluate the results on the test set. 73 | - `csCreateTrainIdLabelImgs`: Convert annotations in polygonal format to png images with label IDs, where pixels encode "train IDs" that you can define in `labels.py`. 74 | - `csCreateTrainIdInstanceImgs`: Convert annotations in polygonal format to png images with instance IDs, where pixels encode instance IDs composed of "train IDs". 75 | - `csCreatePanopticImgs`: Convert annotations in standard png format to [COCO panoptic segmentation format](http://cocodataset.org/#format-data). 76 | - `csPlot3dDetectionResults`: Visualize 3D object detection evaluation results stored in .json format. 77 | 78 | 79 | ### Package Content 80 | 81 | The package is structured as follows 82 | - `helpers`: helper files that are included by other scripts 83 | - `viewer`: view the images and the annotations 84 | - `preparation`: convert the ground truth annotations into a format suitable for your approach 85 | - `evaluation`: validate your approach 86 | - `annotation`: the annotation tool used for labeling the dataset 87 | - `download`: downloader for Cityscapes packages 88 | 89 | Note that all files have a small documentation at the top. Most important files 90 | - `helpers/labels.py`: central file defining the IDs of all semantic classes and providing mapping between various class properties. 91 | - `helpers/labels_cityPersons.py`: file defining the IDs of all CityPersons pedestrian classes and providing mapping between various class properties. 92 | - `setup.py`: run `CYTHONIZE_EVAL= python setup.py build_ext --inplace` to enable cython plugin for faster evaluation. Only tested for Ubuntu. 93 | 94 | 95 | ## Evaluation 96 | 97 | Once you want to test your method on the test set, please run your approach on the provided test images and submit your results: 98 | [Submission Page](www.cityscapes-dataset.com/submit) 99 | 100 | The result format is described at the top of our evaluation scripts: 101 | - [Pixel Level Semantic Labeling](cityscapesscripts/evaluation/evalPixelLevelSemanticLabeling.py) 102 | - [Instance Level Semantic Labeling](cityscapesscripts/evaluation/evalInstanceLevelSemanticLabeling.py) 103 | - [Panoptic Semantic Labeling](cityscapesscripts/evaluation/evalPanopticSemanticLabeling.py) 104 | - [3D Object Detection](cityscapesscripts/evaluation/evalObjectDetection3d.py) 105 | 106 | Note that our evaluation scripts are included in the scripts folder and can be used to test your approach on the validation set. For further details regarding the submission process, please consult our website. 107 | 108 | ## License 109 | 110 | The dataset itself is released under custom [terms and conditions](https://www.cityscapes-dataset.com/license/). 111 | 112 | The Cityscapes Scripts are released under MIT license as found in the [license file](LICENSE). 113 | 114 | ## Contact 115 | 116 | Please feel free to contact us with any questions, suggestions or comments: 117 | 118 | * Marius Cordts, Mohamed Omran 119 | * mail@cityscapes-dataset.net 120 | * www.cityscapes-dataset.com 121 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/Box3DImageTransform.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Box 3D Image Transform\n", 8 | "This notebook is intended to demonstrate the differences of the different coordinate systems used for 3D Boxes.\n", 9 | "In general, 4 different coordinate systems are used with 3 of them are described in https://github.com/mcordts/cityscapesScripts/blob/master/docs/csCalibration.pdf\n", 10 | "1. The vehicle coordinate system *V* according to ISO 8855 with the origin on the ground below of the rear axis center, *x* pointing in driving direction, *y* pointing left, and *z* pointing up.\n", 11 | "2. The camera coordinate system *C* with the origin in the camera’s optical center and same orientation as *V*.\n", 12 | "3. The image coordinate system *I* with the origin in the top-left image pixel, *u* pointing right, and *v* pointing down.\n", 13 | "4. In addition, we also add the coordinate system *S* with the same origin as *C*, but the orientation of *I*, ie. *x* pointing right, *y* down, and *z* in the driving direction.\n", 14 | "\n", 15 | "All GT annotations are given in the ISO coordinate system *V* and hence, the evaluation requires the data to be available in this coordinate system.\n", 16 | "\n", 17 | "In this notebook, the transformations between all these coordinate frames are described exemplarily by loading a 3D box annotation and calculate the projection into 2D image, ie. coordinate system *I*." 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "### Sample annotation" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "sample_annotation = {\n", 34 | " \"imgWidth\": 2048,\n", 35 | " \"imgHeight\": 1024,\n", 36 | " \"sensor\": {\n", 37 | " \"sensor_T_ISO_8855\": [\n", 38 | " [\n", 39 | " 0.9990881051503779,\n", 40 | " -0.01948468779721943,\n", 41 | " -0.03799085532693703,\n", 42 | " -1.6501524664770573\n", 43 | " ],\n", 44 | " [\n", 45 | " 0.019498764210995674,\n", 46 | " 0.9998098810245096,\n", 47 | " 0.0,\n", 48 | " -0.1331288872611436\n", 49 | " ],\n", 50 | " [\n", 51 | " 0.03798363254444427,\n", 52 | " -0.0007407747301939942,\n", 53 | " 0.9992780868764849,\n", 54 | " -1.2836173638418473\n", 55 | " ]\n", 56 | " ],\n", 57 | " \"fx\": 2262.52,\n", 58 | " \"fy\": 2265.3017905988554,\n", 59 | " \"u0\": 1096.98,\n", 60 | " \"v0\": 513.137,\n", 61 | " \"baseline\": 0.209313\n", 62 | " },\n", 63 | " \"objects\": [\n", 64 | " {\n", 65 | " \"2d\": {\n", 66 | " \"modal\": [\n", 67 | " 609,\n", 68 | " 420,\n", 69 | " 198,\n", 70 | " 111\n", 71 | " ],\n", 72 | " \"amodal\": [\n", 73 | " 602,\n", 74 | " 415,\n", 75 | " 214,\n", 76 | " 118\n", 77 | " ]\n", 78 | " },\n", 79 | " \"3d\": {\n", 80 | " \"center\": [\n", 81 | " 33.95,\n", 82 | " 5.05,\n", 83 | " 0.57\n", 84 | " ],\n", 85 | " \"dimensions\": [\n", 86 | " 4.3,\n", 87 | " 1.72,\n", 88 | " 1.53\n", 89 | " ],\n", 90 | " \"rotation\": [\n", 91 | " 0.9735839424380041,\n", 92 | " -0.010751769161021867,\n", 93 | " 0.0027191710555974913,\n", 94 | " 0.22805988817753894\n", 95 | " ],\n", 96 | " \"type\": \"Mid Size Car\",\n", 97 | " \"format\": \"CRS_ISO8855\"\n", 98 | " },\n", 99 | " \"occlusion\": 0.0,\n", 100 | " \"truncation\": 0.0,\n", 101 | " \"instanceId\": 26010,\n", 102 | " \"label\": \"car\",\n", 103 | " \"score\": 1.0\n", 104 | " }\n", 105 | " ]\n", 106 | "}" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "### Python imports" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": null, 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "import numpy as np\n", 123 | "from cityscapesscripts.helpers.annotation import CsBbox3d\n", 124 | "from cityscapesscripts.helpers.box3dImageTransform import (\n", 125 | " Camera, \n", 126 | " Box3dImageTransform,\n", 127 | " CRS_V,\n", 128 | " CRS_C,\n", 129 | " CRS_S\n", 130 | ")" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": {}, 136 | "source": [ 137 | "### Create the camera\n", 138 | "``sensor_T_ISO_8855`` is the transformation matrix from coordinate system *V* to *C*." 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": null, 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [ 147 | "camera = Camera(fx=sample_annotation[\"sensor\"][\"fx\"],\n", 148 | " fy=sample_annotation[\"sensor\"][\"fy\"],\n", 149 | " u0=sample_annotation[\"sensor\"][\"u0\"],\n", 150 | " v0=sample_annotation[\"sensor\"][\"v0\"],\n", 151 | " sensor_T_ISO_8855=sample_annotation[\"sensor\"][\"sensor_T_ISO_8855\"])" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "### Load the annotation\n", 159 | "As the annotation is given in coordinate system *V*, it must be transformed from *V* → *C* → *S* → *I*." 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "# Create the Box3dImageTransform object\n", 169 | "box3d_annotation = Box3dImageTransform(camera=camera)\n", 170 | "\n", 171 | "# Create a CsBox3d object for the 3D annotation\n", 172 | "obj = CsBbox3d()\n", 173 | "obj.fromJsonText(sample_annotation[\"objects\"][0])\n", 174 | "\n", 175 | "# Initialize the 3D box with an annotation in coordinate system V. \n", 176 | "# You can alternatively pass CRS_S or CRS_C if you want to initalize the box in a different coordinate system.\n", 177 | "# Please note that the object's size is always given as [L, W, H] independently of the used coodrinate system.\n", 178 | "box3d_annotation.initialize_box_from_annotation(obj, coordinate_system=CRS_V)\n", 179 | "size_V, center_V, rotation_V = box3d_annotation.get_parameters(coordinate_system=CRS_V)" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "metadata": {}, 185 | "source": [ 186 | "### Print coordinates of cuboid vertices" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": null, 192 | "metadata": {}, 193 | "outputs": [], 194 | "source": [ 195 | "# Get the vertices of the 3D box in the requested coordinate frame\n", 196 | "box_vertices_V = box3d_annotation.get_vertices(coordinate_system=CRS_V)\n", 197 | "box_vertices_C = box3d_annotation.get_vertices(coordinate_system=CRS_C)\n", 198 | "box_vertices_S = box3d_annotation.get_vertices(coordinate_system=CRS_S)\n", 199 | "\n", 200 | "# Print the vertices of the box.\n", 201 | "# loc is encoded with a 3-char code\n", 202 | "# 0: B/F: Back or Front\n", 203 | "# 1: L/R: Left or Right\n", 204 | "# 2: B/T: Bottom or Top\n", 205 | "# BLT -> Back left top of the object\n", 206 | "\n", 207 | "# Print in V coordinate system\n", 208 | "print(\"Vertices in V:\")\n", 209 | "print(\" {:>8} {:>8} {:>8}\".format(\"x[m]\", \"y[m]\", \"z[m]\"))\n", 210 | "for loc, coord in box_vertices_V.items():\n", 211 | " print(\"{}: {:8.2f} {:8.2f} {:8.2f}\".format(loc, coord[0], coord[1], coord[2]))\n", 212 | " \n", 213 | "# Print in C coordinate system\n", 214 | "print(\"\\nVertices in C:\")\n", 215 | "print(\" {:>8} {:>8} {:>8}\".format(\"x[m]\", \"y[m]\", \"z[m]\"))\n", 216 | "for loc, coord in box_vertices_C.items():\n", 217 | " print(\"{}: {:8.2f} {:8.2f} {:8.2f}\".format(loc, coord[0], coord[1], coord[2]))\n", 218 | " \n", 219 | "# Print in S coordinate system\n", 220 | "print(\"\\nVertices in S:\")\n", 221 | "print(\" {:>8} {:>8} {:>8}\".format(\"x[m]\", \"y[m]\", \"z[m]\"))\n", 222 | "for loc, coord in box_vertices_S.items():\n", 223 | " print(\"{}: {:8.2f} {:8.2f} {:8.2f}\".format(loc, coord[0], coord[1], coord[2]))" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "metadata": {}, 229 | "source": [ 230 | "### Print box parameters" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": null, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "# Similar to the box vertices, you can retrieve box parameters center, size and rotation in any coordinate system\n", 240 | "size_V, center_V, rotation_V = box3d_annotation.get_parameters(coordinate_system=CRS_V)\n", 241 | "# size_C, center_C, rotation_C = box3d_annotation.get_parameters(coordinate_system=CRS_C)\n", 242 | "# size_S, center_S, rotation_S = box3d_annotation.get_parameters(coordinate_system=CRS_S)\n", 243 | "\n", 244 | "print(\"Size: \", size_V)\n", 245 | "print(\"Center: \", center_V)\n", 246 | "print(\"Rotation:\", rotation_V)" 247 | ] 248 | }, 249 | { 250 | "cell_type": "markdown", 251 | "metadata": {}, 252 | "source": [ 253 | "### Get 2D image coordinates" 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": null, 259 | "metadata": {}, 260 | "outputs": [], 261 | "source": [ 262 | "# Get the vertices of the 3D box in the image coordinates\n", 263 | "box_vertices_I = box3d_annotation.get_vertices_2d()\n", 264 | "\n", 265 | "# Print the vertices of the box.\n", 266 | "# loc is encoded with a 3-char code\n", 267 | "# 0: B/F: Back or Front\n", 268 | "# 1: L/R: Left or Right\n", 269 | "# 2: B/T: Bottom or Top\n", 270 | "# BLT -> Back left top of the object\n", 271 | "\n", 272 | "print(\"\\n {:>8} {:>8}\".format(\"u[px]\", \"v[px]\"))\n", 273 | "for loc, coord in box_vertices_I.items():\n", 274 | " print(\"{}: {:8.2f} {:8.2f}\".format(loc, coord[0], coord[1]))" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "metadata": {}, 280 | "source": [ 281 | "### Exemplarily generate amodal 2D bounding box" 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": null, 287 | "metadata": {}, 288 | "outputs": [], 289 | "source": [ 290 | "# generate amodal 2D box from these values\n", 291 | "xmin = int(min([p[0] for p in box_vertices_I.values()]))\n", 292 | "ymin = int(min([p[1] for p in box_vertices_I.values()]))\n", 293 | "xmax = int(max([p[0] for p in box_vertices_I.values()]))\n", 294 | "ymax = int(max([p[1] for p in box_vertices_I.values()]))\n", 295 | "\n", 296 | "bbox_amodal = [xmin, ymin, xmax, ymax]\n", 297 | "\n", 298 | "print(\"Amodal 2D bounding box\")\n", 299 | "print(bbox_amodal)\n", 300 | "# load from CsBbox3d object, these 2 bounding boxes should be the same\n", 301 | "print(obj.bbox_2d.bbox_amodal)\n", 302 | "\n", 303 | "assert bbox_amodal == obj.bbox_2d.bbox_amodal" 304 | ] 305 | }, 306 | { 307 | "cell_type": "markdown", 308 | "metadata": {}, 309 | "source": [ 310 | "### Check for cycle consistency\n", 311 | "A box initialized in *V* and converted to *S* and *C* and back need to give the initial values. " 312 | ] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": null, 317 | "metadata": {}, 318 | "outputs": [], 319 | "source": [ 320 | "# Initialize box in V\n", 321 | "box3d_annotation.initialize_box(size=sample_annotation[\"objects\"][0][\"3d\"][\"dimensions\"],\n", 322 | " quaternion=sample_annotation[\"objects\"][0][\"3d\"][\"rotation\"],\n", 323 | " center=sample_annotation[\"objects\"][0][\"3d\"][\"center\"],\n", 324 | " coordinate_system=CRS_V)\n", 325 | "size_VV, center_VV, rotation_VV = box3d_annotation.get_parameters(coordinate_system=CRS_V)\n", 326 | "\n", 327 | "# Retrieve parameters in C, initialize in C and retrieve in V\n", 328 | "size_C, center_C, rotation_C = box3d_annotation.get_parameters(coordinate_system=CRS_C)\n", 329 | "box3d_annotation.initialize_box(size=size_C,\n", 330 | " quaternion=rotation_C,\n", 331 | " center=center_C,\n", 332 | " coordinate_system=CRS_C)\n", 333 | "size_VC, center_VC, rotation_VC = box3d_annotation.get_parameters(coordinate_system=CRS_V)\n", 334 | "\n", 335 | "# Retrieve parameters in S, initialize in S and retrieve in V\n", 336 | "size_S, center_S, rotation_S = box3d_annotation.get_parameters(coordinate_system=CRS_S)\n", 337 | "box3d_annotation.initialize_box(size=size_S,\n", 338 | " quaternion=rotation_S,\n", 339 | " center=center_S,\n", 340 | " coordinate_system=CRS_S)\n", 341 | "size_VS, center_VS, rotation_VS = box3d_annotation.get_parameters(coordinate_system=CRS_V)\n", 342 | "\n", 343 | "assert np.isclose(size_VV, size_VC).all() and np.isclose(size_VV, size_VS).all()\n", 344 | "assert np.isclose(center_VV, center_VC).all() and np.isclose(center_VV, center_VS).all()\n", 345 | "assert (rotation_VV == rotation_VC) and (rotation_VV == rotation_VS)" 346 | ] 347 | } 348 | ], 349 | "metadata": { 350 | "kernelspec": { 351 | "display_name": "Python 3", 352 | "language": "python", 353 | "name": "python3" 354 | }, 355 | "language_info": { 356 | "codemirror_mode": { 357 | "name": "ipython", 358 | "version": 3 359 | }, 360 | "file_extension": ".py", 361 | "mimetype": "text/x-python", 362 | "name": "python", 363 | "nbconvert_exporter": "python", 364 | "pygments_lexer": "ipython3", 365 | "version": "3.7.4" 366 | } 367 | }, 368 | "nbformat": 4, 369 | "nbformat_minor": 4 370 | } 371 | -------------------------------------------------------------------------------- /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 | import numpy as np 10 | from collections import namedtuple 11 | 12 | # get current date and time 13 | import datetime 14 | import locale 15 | 16 | from abc import ABCMeta, abstractmethod 17 | from .box3dImageTransform import Camera 18 | 19 | # A point in a polygon 20 | Point = namedtuple('Point', ['x', 'y']) 21 | 22 | 23 | class CsObjectType(): 24 | """Type of an object""" 25 | POLY = 1 # polygon 26 | BBOX2D = 2 # bounding box 27 | BBOX3D = 3 # 3d bounding box 28 | IGNORE2D = 4 # 2d ignore region 29 | 30 | 31 | class CsObject: 32 | """Abstract base class for annotation objects""" 33 | __metaclass__ = ABCMeta 34 | 35 | def __init__(self, objType): 36 | self.objectType = objType 37 | # the label 38 | self.label = "" 39 | 40 | # If deleted or not 41 | self.deleted = 0 42 | # If verified or not 43 | self.verified = 0 44 | # The date string 45 | self.date = "" 46 | # The username 47 | self.user = "" 48 | # Draw the object 49 | # Not read from or written to JSON 50 | # Set to False if deleted object 51 | # Might be set to False by the application for other reasons 52 | self.draw = True 53 | 54 | @abstractmethod 55 | def __str__(self): pass 56 | 57 | @abstractmethod 58 | def fromJsonText(self, jsonText, objId=-1): pass 59 | 60 | @abstractmethod 61 | def toJsonText(self): pass 62 | 63 | def updateDate(self): 64 | try: 65 | locale.setlocale(locale.LC_ALL, 'en_US.utf8') 66 | except locale.Error: 67 | locale.setlocale(locale.LC_ALL, 'en_US') 68 | except locale.Error: 69 | locale.setlocale(locale.LC_ALL, 'us_us.utf8') 70 | except locale.Error: 71 | locale.setlocale(locale.LC_ALL, 'us_us') 72 | except Exception: 73 | pass 74 | self.date = datetime.datetime.now().strftime("%d-%b-%Y %H:%M:%S") 75 | 76 | # Mark the object as deleted 77 | def delete(self): 78 | self.deleted = 1 79 | self.draw = False 80 | 81 | 82 | class CsPoly(CsObject): 83 | """Class that contains the information of a single annotated object as polygon""" 84 | 85 | # Constructor 86 | def __init__(self): 87 | CsObject.__init__(self, CsObjectType.POLY) 88 | # the polygon as list of points 89 | self.polygon = [] 90 | # the object ID 91 | self.id = -1 92 | 93 | def __str__(self): 94 | polyText = "" 95 | if self.polygon: 96 | if len(self.polygon) <= 4: 97 | for p in self.polygon: 98 | polyText += '({},{}) '.format(p.x, p.y) 99 | else: 100 | polyText += '({},{}) ({},{}) ... ({},{}) ({},{})'.format( 101 | self.polygon[0].x, self.polygon[0].y, 102 | self.polygon[1].x, self.polygon[1].y, 103 | self.polygon[-2].x, self.polygon[-2].y, 104 | self.polygon[-1].x, self.polygon[-1].y) 105 | else: 106 | polyText = "none" 107 | text = "Object: {} - {}".format(self.label, polyText) 108 | return text 109 | 110 | def fromJsonText(self, jsonText, objId=-1): 111 | self.id = objId 112 | self.label = str(jsonText['label']) 113 | self.polygon = [Point(p[0], p[1]) for p in jsonText['polygon']] 114 | if 'deleted' in jsonText.keys(): 115 | self.deleted = jsonText['deleted'] 116 | else: 117 | self.deleted = 0 118 | if 'verified' in jsonText.keys(): 119 | self.verified = jsonText['verified'] 120 | else: 121 | self.verified = 1 122 | if 'user' in jsonText.keys(): 123 | self.user = jsonText['user'] 124 | else: 125 | self.user = '' 126 | if 'date' in jsonText.keys(): 127 | self.date = jsonText['date'] 128 | else: 129 | self.date = '' 130 | if self.deleted == 1: 131 | self.draw = False 132 | else: 133 | self.draw = True 134 | 135 | def toJsonText(self): 136 | objDict = {} 137 | objDict['label'] = self.label 138 | objDict['id'] = self.id 139 | objDict['deleted'] = self.deleted 140 | objDict['verified'] = self.verified 141 | objDict['user'] = self.user 142 | objDict['date'] = self.date 143 | objDict['polygon'] = [] 144 | for pt in self.polygon: 145 | objDict['polygon'].append([pt.x, pt.y]) 146 | 147 | return objDict 148 | 149 | 150 | class CsBbox2d(CsObject): 151 | """Class that contains the information of a single annotated object as bounding box""" 152 | 153 | # Constructor 154 | def __init__(self): 155 | CsObject.__init__(self, CsObjectType.BBOX2D) 156 | # the polygon as list of points 157 | self.bbox_amodal_xywh = [] 158 | self.bbox_modal_xywh = [] 159 | 160 | # the ID of the corresponding object 161 | self.instanceId = -1 162 | # the label of the corresponding object 163 | self.label = "" 164 | 165 | def __str__(self): 166 | bboxAmodalText = "" 167 | bboxAmodalText += '[(x1: {}, y1: {}), (w: {}, h: {})]'.format( 168 | self.bbox_amodal_xywh[0], self.bbox_amodal_xywh[1], self.bbox_amodal_xywh[2], self.bbox_amodal_xywh[3]) 169 | 170 | bboxModalText = "" 171 | bboxModalText += '[(x1: {}, y1: {}), (w: {}, h: {})]'.format( 172 | self.bbox_modal_xywh[0], self.bbox_modal_xywh[1], self.bbox_modal_xywh[2], self.bbox_modal_xywh[3]) 173 | 174 | text = "Object: {}\n - Amodal {}\n - Modal {}".format( 175 | self.label, bboxAmodalText, bboxModalText) 176 | return text 177 | 178 | def setAmodalBox(self, bbox_amodal): 179 | # sets the amodal box if required 180 | self.bbox_amodal_xywh = [ 181 | bbox_amodal[0], 182 | bbox_amodal[1], 183 | bbox_amodal[2] - bbox_amodal[0], 184 | bbox_amodal[3] - bbox_amodal[1] 185 | ] 186 | 187 | # access 2d boxes in [xmin, ymin, xmax, ymax] format 188 | @property 189 | def bbox_amodal(self): 190 | """Returns the 2d box as [xmin, ymin, xmax, ymax]""" 191 | return [ 192 | self.bbox_amodal_xywh[0], 193 | self.bbox_amodal_xywh[1], 194 | self.bbox_amodal_xywh[0] + self.bbox_amodal_xywh[2], 195 | self.bbox_amodal_xywh[1] + self.bbox_amodal_xywh[3] 196 | ] 197 | 198 | @property 199 | def bbox_modal(self): 200 | """Returns the 2d box as [xmin, ymin, xmax, ymax]""" 201 | return [ 202 | self.bbox_modal_xywh[0], 203 | self.bbox_modal_xywh[1], 204 | self.bbox_modal_xywh[0] + self.bbox_modal_xywh[2], 205 | self.bbox_modal_xywh[1] + self.bbox_modal_xywh[3] 206 | ] 207 | 208 | def fromJsonText(self, jsonText, objId=-1): 209 | # try to load from cityperson format 210 | if 'bbox' in jsonText.keys() and 'bboxVis' in jsonText.keys(): 211 | self.bbox_amodal_xywh = jsonText['bbox'] 212 | self.bbox_modal_xywh = jsonText['bboxVis'] 213 | # both modal and amodal boxes are provided 214 | elif "modal" in jsonText.keys() and "amodal" in jsonText.keys(): 215 | self.bbox_amodal_xywh = jsonText['amodal'] 216 | self.bbox_modal_xywh = jsonText['modal'] 217 | # only amodal boxes are provided 218 | else: 219 | self.bbox_modal_xywh = jsonText['amodal'] 220 | self.bbox_amodal_xywh = jsonText['amodal'] 221 | 222 | # load label and instanceId if available 223 | if 'label' in jsonText.keys() and 'instanceId' in jsonText.keys(): 224 | self.label = str(jsonText['label']) 225 | self.instanceId = jsonText['instanceId'] 226 | 227 | def toJsonText(self): 228 | objDict = {} 229 | objDict['label'] = self.label 230 | objDict['instanceId'] = self.instanceId 231 | objDict['modal'] = self.bbox_modal_xywh 232 | objDict['amodal'] = self.bbox_amodal_xywh 233 | 234 | return objDict 235 | 236 | 237 | class CsBbox3d(CsObject): 238 | """Class that contains the information of a single annotated object as 3D bounding box""" 239 | 240 | # Constructor 241 | def __init__(self): 242 | CsObject.__init__(self, CsObjectType.BBOX3D) 243 | 244 | self.bbox_2d = None 245 | 246 | self.center = [] 247 | self.dims = [] 248 | self.rotation = [] 249 | self.instanceId = -1 250 | self.label = "" 251 | self.score = -1. 252 | 253 | def __str__(self): 254 | bbox2dText = str(self.bbox_2d) 255 | 256 | bbox3dText = "" 257 | bbox3dText += '\n - Center (x/y/z) [m]: {}/{}/{}'.format( 258 | self.center[0], self.center[1], self.center[2]) 259 | bbox3dText += '\n - Dimensions (l/w/h) [m]: {}/{}/{}'.format( 260 | self.dims[0], self.dims[1], self.dims[2]) 261 | bbox3dText += '\n - Rotation: {}/{}/{}/{}'.format( 262 | self.rotation[0], self.rotation[1], self.rotation[2], self.rotation[3]) 263 | 264 | text = "Object: {}\n2D {}\n - 3D {}".format( 265 | self.label, bbox2dText, bbox3dText) 266 | return text 267 | 268 | def fromJsonText(self, jsonText, objId=-1): 269 | # load 2D box 270 | self.bbox_2d = CsBbox2d() 271 | self.bbox_2d.fromJsonText(jsonText['2d']) 272 | 273 | self.center = jsonText['3d']['center'] 274 | self.dims = jsonText['3d']['dimensions'] 275 | self.rotation = jsonText['3d']['rotation'] 276 | self.label = jsonText['label'] 277 | self.score = jsonText['score'] 278 | 279 | if 'instanceId' in jsonText.keys(): 280 | self.instanceId = jsonText['instanceId'] 281 | 282 | def toJsonText(self): 283 | objDict = {} 284 | objDict['label'] = self.label 285 | objDict['instanceId'] = self.instanceId 286 | objDict['2d']['amodal'] = self.bbox_2d.bbox_amodal_xywh 287 | objDict['2d']['modal'] = self.bbox_2d.bbox_modal_xywh 288 | objDict['3d']['center'] = self.center 289 | objDict['3d']['dimensions'] = self.dims 290 | objDict['3d']['rotation'] = self.rotation 291 | 292 | return objDict 293 | 294 | @property 295 | def depth(self): 296 | # returns the BEV depth 297 | return np.sqrt(self.center[0]**2 + self.center[1]**2).astype(int) 298 | 299 | 300 | class CsIgnore2d(CsObject): 301 | """Class that contains the information of a single annotated 2d ignore region""" 302 | 303 | # Constructor 304 | def __init__(self): 305 | CsObject.__init__(self, CsObjectType.IGNORE2D) 306 | 307 | self.bbox_xywh = [] 308 | self.label = "" 309 | self.instanceId = -1 310 | 311 | def __str__(self): 312 | bbox2dText = "" 313 | bbox2dText += 'Ignore Region: (x1: {}, y1: {}), (w: {}, h: {})'.format( 314 | self.bbox_xywh[0], self.bbox_xywh[1], self.bbox_xywh[2], self.bbox_xywh[3]) 315 | 316 | return bbox2dText 317 | 318 | def fromJsonText(self, jsonText, objId=-1): 319 | self.bbox_xywh = jsonText['2d'] 320 | 321 | if 'label' in jsonText.keys(): 322 | self.label = jsonText['label'] 323 | 324 | if 'instanceId' in jsonText.keys(): 325 | self.instanceId = jsonText['instanceId'] 326 | 327 | def toJsonText(self): 328 | objDict = {} 329 | objDict['label'] = self.label 330 | objDict['instanceId'] = self.instanceId 331 | objDict['2d'] = self.bbox_xywh 332 | 333 | return objDict 334 | 335 | @property 336 | def bbox(self): 337 | """Returns the 2d box as [xmin, ymin, xmax, ymax]""" 338 | return [ 339 | self.bbox_xywh[0], 340 | self.bbox_xywh[1], 341 | self.bbox_xywh[0] + self.bbox_xywh[2], 342 | self.bbox_xywh[1] + self.bbox_xywh[3] 343 | ] 344 | 345 | # Extend api to be compatible to bbox2d 346 | @property 347 | def bbox_amodal_xywh(self): 348 | return self.bbox_xywh 349 | 350 | @property 351 | def bbox_modal_xywh(self): 352 | return self.bbox_xywh 353 | 354 | 355 | class Annotation: 356 | """The annotation of a whole image (doesn't support mixed annotations, i.e. combining CsPoly and CsBbox2d)""" 357 | 358 | # Constructor 359 | def __init__(self, objType=CsObjectType.POLY): 360 | # the width of that image and thus of the label image 361 | self.imgWidth = 0 362 | # the height of that image and thus of the label image 363 | self.imgHeight = 0 364 | # the list of objects 365 | self.objects = [] 366 | # the camera calibration 367 | self.camera = None 368 | assert objType in CsObjectType.__dict__.values() 369 | self.objectType = objType 370 | 371 | def toJson(self): 372 | return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) 373 | 374 | def fromJsonText(self, jsonText): 375 | jsonDict = json.loads(jsonText) 376 | self.imgWidth = int(jsonDict['imgWidth']) 377 | self.imgHeight = int(jsonDict['imgHeight']) 378 | self.objects = [] 379 | # load objects 380 | if self.objectType != CsObjectType.IGNORE2D: 381 | for objId, objIn in enumerate(jsonDict['objects']): 382 | if self.objectType == CsObjectType.POLY: 383 | obj = CsPoly() 384 | elif self.objectType == CsObjectType.BBOX2D: 385 | obj = CsBbox2d() 386 | elif self.objectType == CsObjectType.BBOX3D: 387 | obj = CsBbox3d() 388 | obj.fromJsonText(objIn, objId) 389 | self.objects.append(obj) 390 | 391 | # load ignores 392 | if 'ignore' in jsonDict.keys(): 393 | for ignoreId, ignoreIn in enumerate(jsonDict['ignore']): 394 | obj = CsIgnore2d() 395 | obj.fromJsonText(ignoreIn, ignoreId) 396 | self.objects.append(obj) 397 | 398 | # load camera calibration 399 | if 'sensor' in jsonDict.keys(): 400 | self.camera = Camera(fx=jsonDict['sensor']['fx'], 401 | fy=jsonDict['sensor']['fy'], 402 | u0=jsonDict['sensor']['u0'], 403 | v0=jsonDict['sensor']['v0'], 404 | sensor_T_ISO_8855=jsonDict['sensor']['sensor_T_ISO_8855']) 405 | 406 | def toJsonText(self): 407 | jsonDict = {} 408 | jsonDict['imgWidth'] = self.imgWidth 409 | jsonDict['imgHeight'] = self.imgHeight 410 | jsonDict['objects'] = [] 411 | for obj in self.objects: 412 | objDict = obj.toJsonText() 413 | jsonDict['objects'].append(objDict) 414 | 415 | return jsonDict 416 | 417 | # Read a json formatted polygon file and return the annotation 418 | def fromJsonFile(self, jsonFile): 419 | if not os.path.isfile(jsonFile): 420 | print('Given json file not found: {}'.format(jsonFile)) 421 | return 422 | with open(jsonFile, 'r') as f: 423 | jsonText = f.read() 424 | self.fromJsonText(jsonText) 425 | 426 | def toJsonFile(self, jsonFile): 427 | with open(jsonFile, 'w') as f: 428 | f.write(self.toJson()) 429 | 430 | 431 | # a dummy example 432 | if __name__ == "__main__": 433 | obj = CsPoly() 434 | obj.label = 'car' 435 | obj.polygon.append(Point(0, 0)) 436 | obj.polygon.append(Point(1, 0)) 437 | obj.polygon.append(Point(1, 1)) 438 | obj.polygon.append(Point(0, 1)) 439 | 440 | print(type(obj).__name__) 441 | print(obj) 442 | -------------------------------------------------------------------------------- /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 | from PIL import Image 35 | 36 | # Cityscapes imports 37 | from cityscapesscripts.helpers.csHelpers import printError 38 | from cityscapesscripts.helpers.labels import labels as csLabels 39 | 40 | 41 | OFFSET = 256 * 256 * 256 42 | VOID = 0 43 | 44 | 45 | # The decorator is used to prints an error trhown inside process 46 | def get_traceback(f): 47 | @functools.wraps(f) 48 | def wrapper(*args, **kwargs): 49 | try: 50 | return f(*args, **kwargs) 51 | except Exception as e: 52 | print('Caught exception in worker thread:') 53 | traceback.print_exc() 54 | raise e 55 | 56 | return wrapper 57 | 58 | 59 | def rgb2id(color): 60 | if isinstance(color, np.ndarray) and len(color.shape) == 3: 61 | if color.dtype == np.uint8: 62 | color = color.astype(np.int32) 63 | return color[:, :, 0] + 256 * color[:, :, 1] + 256 * 256 * color[:, :, 2] 64 | return int(color[0] + 256 * color[1] + 256 * 256 * color[2]) 65 | 66 | 67 | class PQStatCat(): 68 | def __init__(self): 69 | self.iou = 0.0 70 | self.tp = 0 71 | self.fp = 0 72 | self.fn = 0 73 | 74 | def __iadd__(self, pq_stat_cat): 75 | self.iou += pq_stat_cat.iou 76 | self.tp += pq_stat_cat.tp 77 | self.fp += pq_stat_cat.fp 78 | self.fn += pq_stat_cat.fn 79 | return self 80 | 81 | 82 | class PQStat(): 83 | def __init__(self): 84 | self.pq_per_cat = defaultdict(PQStatCat) 85 | 86 | def __getitem__(self, i): 87 | return self.pq_per_cat[i] 88 | 89 | def __iadd__(self, pq_stat): 90 | for label, pq_stat_cat in pq_stat.pq_per_cat.items(): 91 | self.pq_per_cat[label] += pq_stat_cat 92 | return self 93 | 94 | def pq_average(self, categories, isthing): 95 | pq, sq, rq, n = 0, 0, 0, 0 96 | per_class_results = {} 97 | for label, label_info in categories.items(): 98 | if isthing is not None: 99 | cat_isthing = label_info['isthing'] == 1 100 | if isthing != cat_isthing: 101 | continue 102 | iou = self.pq_per_cat[label].iou 103 | tp = self.pq_per_cat[label].tp 104 | fp = self.pq_per_cat[label].fp 105 | fn = self.pq_per_cat[label].fn 106 | if tp + fp + fn == 0: 107 | per_class_results[label] = {'pq': 0.0, 'sq': 0.0, 'rq': 0.0} 108 | continue 109 | n += 1 110 | pq_class = iou / (tp + 0.5 * fp + 0.5 * fn) 111 | sq_class = iou / tp if tp != 0 else 0 112 | rq_class = tp / (tp + 0.5 * fp + 0.5 * fn) 113 | per_class_results[label] = {'pq': pq_class, 'sq': sq_class, 'rq': rq_class} 114 | pq += pq_class 115 | sq += sq_class 116 | rq += rq_class 117 | 118 | return {'pq': pq / n, 'sq': sq / n, 'rq': rq / n, 'n': n}, per_class_results 119 | 120 | 121 | @get_traceback 122 | def pq_compute_single_core(proc_id, annotation_set, gt_folder, pred_folder, categories): 123 | pq_stat = PQStat() 124 | 125 | idx = 0 126 | for gt_ann, pred_ann in annotation_set: 127 | if idx % 30 == 0: 128 | print('Core: {}, {} from {} images processed'.format(proc_id, idx, len(annotation_set))) 129 | idx += 1 130 | 131 | pan_gt = np.array(Image.open(os.path.join(gt_folder, gt_ann['file_name'])), dtype=np.uint32) 132 | pan_gt = rgb2id(pan_gt) 133 | pan_pred = np.array(Image.open(os.path.join(pred_folder, pred_ann['file_name'])), dtype=np.uint32) 134 | pan_pred = rgb2id(pan_pred) 135 | 136 | gt_segms = {el['id']: el for el in gt_ann['segments_info']} 137 | pred_segms = {el['id']: el for el in pred_ann['segments_info']} 138 | 139 | # predicted segments area calculation + prediction sanity checks 140 | pred_labels_set = set(el['id'] for el in pred_ann['segments_info']) 141 | labels, labels_cnt = np.unique(pan_pred, return_counts=True) 142 | for label, label_cnt in zip(labels, labels_cnt): 143 | if label not in pred_segms: 144 | if label == VOID: 145 | continue 146 | 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)) 147 | pred_segms[label]['area'] = label_cnt 148 | pred_labels_set.remove(label) 149 | if pred_segms[label]['category_id'] not in categories: 150 | 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'])) 151 | if len(pred_labels_set) != 0: 152 | 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))) 153 | 154 | # confusion matrix calculation 155 | pan_gt_pred = pan_gt.astype(np.uint64) * OFFSET + pan_pred.astype(np.uint64) 156 | gt_pred_map = {} 157 | labels, labels_cnt = np.unique(pan_gt_pred, return_counts=True) 158 | for label, intersection in zip(labels, labels_cnt): 159 | gt_id = label // OFFSET 160 | pred_id = label % OFFSET 161 | gt_pred_map[(gt_id, pred_id)] = intersection 162 | 163 | # count all matched pairs 164 | gt_matched = set() 165 | pred_matched = set() 166 | for label_tuple, intersection in gt_pred_map.items(): 167 | gt_label, pred_label = label_tuple 168 | if gt_label not in gt_segms: 169 | continue 170 | if pred_label not in pred_segms: 171 | continue 172 | if gt_segms[gt_label]['iscrowd'] == 1: 173 | continue 174 | if gt_segms[gt_label]['category_id'] != pred_segms[pred_label]['category_id']: 175 | continue 176 | 177 | union = pred_segms[pred_label]['area'] + gt_segms[gt_label]['area'] - intersection - gt_pred_map.get((VOID, pred_label), 0) 178 | iou = intersection / union 179 | if iou > 0.5: 180 | pq_stat[gt_segms[gt_label]['category_id']].tp += 1 181 | pq_stat[gt_segms[gt_label]['category_id']].iou += iou 182 | gt_matched.add(gt_label) 183 | pred_matched.add(pred_label) 184 | 185 | # count false positives 186 | crowd_labels_dict = {} 187 | for gt_label, gt_info in gt_segms.items(): 188 | if gt_label in gt_matched: 189 | continue 190 | # crowd segments are ignored 191 | if gt_info['iscrowd'] == 1: 192 | crowd_labels_dict[gt_info['category_id']] = gt_label 193 | continue 194 | pq_stat[gt_info['category_id']].fn += 1 195 | 196 | # count false positives 197 | for pred_label, pred_info in pred_segms.items(): 198 | if pred_label in pred_matched: 199 | continue 200 | # intersection of the segment with VOID 201 | intersection = gt_pred_map.get((VOID, pred_label), 0) 202 | # plus intersection with corresponding CROWD region if it exists 203 | if pred_info['category_id'] in crowd_labels_dict: 204 | intersection += gt_pred_map.get((crowd_labels_dict[pred_info['category_id']], pred_label), 0) 205 | # predicted segment is ignored if more than half of the segment correspond to VOID and CROWD regions 206 | if intersection / pred_info['area'] > 0.5: 207 | continue 208 | pq_stat[pred_info['category_id']].fp += 1 209 | print('Core: {}, all {} images processed'.format(proc_id, len(annotation_set))) 210 | return pq_stat 211 | 212 | 213 | def pq_compute_multi_core(matched_annotations_list, gt_folder, pred_folder, categories): 214 | cpu_num = multiprocessing.cpu_count() 215 | annotations_split = np.array_split(matched_annotations_list, cpu_num) 216 | print("Number of cores: {}, images per core: {}".format(cpu_num, len(annotations_split[0]))) 217 | workers = multiprocessing.Pool(processes=cpu_num) 218 | processes = [] 219 | for proc_id, annotation_set in enumerate(annotations_split): 220 | p = workers.apply_async(pq_compute_single_core, 221 | (proc_id, annotation_set, gt_folder, pred_folder, categories)) 222 | processes.append(p) 223 | pq_stat = PQStat() 224 | for p in processes: 225 | pq_stat += p.get() 226 | workers.close() 227 | return pq_stat 228 | 229 | 230 | def average_pq(pq_stat, categories): 231 | metrics = [("All", None), ("Things", True), ("Stuff", False)] 232 | results = {} 233 | for name, isthing in metrics: 234 | results[name], per_class_results = pq_stat.pq_average(categories, isthing=isthing) 235 | if name == 'All': 236 | results['per_class'] = per_class_results 237 | return results 238 | 239 | 240 | def print_results(results, categories): 241 | metrics = ["All", "Things", "Stuff"] 242 | print("{:14s}| {:>5s} {:>5s} {:>5s}".format("Category", "PQ", "SQ", "RQ")) 243 | labels = sorted(results['per_class'].keys()) 244 | for label in labels: 245 | print("{:14s}| {:5.1f} {:5.1f} {:5.1f}".format( 246 | categories[label]['name'], 247 | 100 * results['per_class'][label]['pq'], 248 | 100 * results['per_class'][label]['sq'], 249 | 100 * results['per_class'][label]['rq'] 250 | )) 251 | print("-" * 41) 252 | print("{:14s}| {:>5s} {:>5s} {:>5s} {:>5s}".format("", "PQ", "SQ", "RQ", "N")) 253 | 254 | for name in metrics: 255 | print("{:14s}| {:5.1f} {:5.1f} {:5.1f} {:5d}".format( 256 | name, 257 | 100 * results[name]['pq'], 258 | 100 * results[name]['sq'], 259 | 100 * results[name]['rq'], 260 | results[name]['n'] 261 | )) 262 | 263 | 264 | def evaluatePanoptic(gt_json_file, gt_folder, pred_json_file, pred_folder, resultsFile): 265 | 266 | start_time = time.time() 267 | with open(gt_json_file, 'r') as f: 268 | gt_json = json.load(f) 269 | with open(pred_json_file, 'r') as f: 270 | pred_json = json.load(f) 271 | categories = {el['id']: el for el in gt_json['categories']} 272 | 273 | print("Evaluation panoptic segmentation metrics:") 274 | print("Ground truth:") 275 | print("\tSegmentation folder: {}".format(gt_folder)) 276 | print("\tJSON file: {}".format(gt_json_file)) 277 | print("Prediction:") 278 | print("\tSegmentation folder: {}".format(pred_folder)) 279 | print("\tJSON file: {}".format(pred_json_file)) 280 | 281 | if not os.path.isdir(gt_folder): 282 | printError("Folder {} with ground truth segmentations doesn't exist".format(gt_folder)) 283 | if not os.path.isdir(pred_folder): 284 | printError("Folder {} with predicted segmentations doesn't exist".format(pred_folder)) 285 | 286 | pred_annotations = {el['image_id']: el for el in pred_json['annotations']} 287 | matched_annotations_list = [] 288 | for gt_ann in gt_json['annotations']: 289 | image_id = gt_ann['image_id'] 290 | if image_id not in pred_annotations: 291 | raise Exception('no prediction for the image with id: {}'.format(image_id)) 292 | matched_annotations_list.append((gt_ann, pred_annotations[image_id])) 293 | 294 | pq_stat = pq_compute_multi_core(matched_annotations_list, gt_folder, pred_folder, categories) 295 | 296 | results = average_pq(pq_stat, categories) 297 | with open(resultsFile, 'w') as f: 298 | print("Saving computed results in {}".format(resultsFile)) 299 | json.dump(results, f, sort_keys=True, indent=4) 300 | print_results(results, categories) 301 | 302 | t_delta = time.time() - start_time 303 | print("Time elapsed: {:0.2f} seconds".format(t_delta)) 304 | 305 | return results 306 | 307 | 308 | # The main method 309 | def main(): 310 | cityscapesPath = os.environ.get( 311 | 'CITYSCAPES_DATASET', os.path.join(os.path.dirname(os.path.realpath(__file__)),'..','..') 312 | ) 313 | gtJsonFile = os.path.join(cityscapesPath, "gtFine", "cityscapes_panoptic_val.json") 314 | 315 | predictionPath = os.environ.get( 316 | 'CITYSCAPES_RESULTS', 317 | os.path.join(cityscapesPath, "results") 318 | ) 319 | predictionJsonFile = os.path.join(predictionPath, "cityscapes_panoptic_val.json") 320 | 321 | parser = argparse.ArgumentParser() 322 | parser.add_argument("--gt-json-file", 323 | dest="gtJsonFile", 324 | help= '''path to json file that contains ground truth in COCO panoptic format. 325 | By default it is $CITYSCAPES_DATASET/gtFine/cityscapes_panoptic_val.json. 326 | ''', 327 | default=gtJsonFile, 328 | type=str) 329 | parser.add_argument("--gt-folder", 330 | dest="gtFolder", 331 | help= '''path to folder that contains ground truth *.png files. If the 332 | argument is not provided this script will look for the *.png files in 333 | 'name' if --gt-json-file set to 'name.json'. 334 | ''', 335 | default=None, 336 | type=str) 337 | parser.add_argument("--prediction-json-file", 338 | dest="predictionJsonFile", 339 | help='''path to json file that contains prediction in COCO panoptic format. 340 | By default is either $CITYSCAPES_RESULTS/cityscapes_panoptic_val.json 341 | or $CITYSCAPES_DATASET/results/cityscapes_panoptic_val.json if 342 | $CITYSCAPES_RESULTS is not set. 343 | ''', 344 | default=predictionJsonFile, 345 | type=str) 346 | parser.add_argument("--prediction-folder", 347 | dest="predictionFolder", 348 | help='''path to folder that contains prediction *.png files. If the 349 | argument is not provided this script will look for the *.png files in 350 | 'name' if --prediction-json-file set to 'name.json'. 351 | ''', 352 | default=None, 353 | type=str) 354 | resultFile = "resultPanopticSemanticLabeling.json" 355 | parser.add_argument("--results_file", 356 | dest="resultsFile", 357 | help="File to store computed panoptic quality. Default: {}".format(resultFile), 358 | default=resultFile, 359 | type=str) 360 | args = parser.parse_args() 361 | 362 | if not os.path.isfile(args.gtJsonFile): 363 | printError("Could not find a ground truth json file in {}. Please run the script with '--help'".format(args.gtJsonFile)) 364 | if args.gtFolder is None: 365 | args.gtFolder = os.path.splitext(args.gtJsonFile)[0] 366 | 367 | if not os.path.isfile(args.predictionJsonFile): 368 | printError("Could not find a prediction json file in {}. Please run the script with '--help'".format(args.predictionJsonFile)) 369 | if args.predictionFolder is None: 370 | args.predictionFolder = os.path.splitext(args.predictionJsonFile)[0] 371 | 372 | evaluatePanoptic(args.gtJsonFile, args.gtFolder, args.predictionJsonFile, args.predictionFolder, args.resultsFile) 373 | 374 | return 375 | 376 | # call the main method 377 | if __name__ == "__main__": 378 | main() 379 | -------------------------------------------------------------------------------- /cityscapesscripts/helpers/box3dImageTransform.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function, absolute_import, division 5 | 6 | import numpy as np 7 | from pyquaternion import Quaternion 8 | 9 | """ 10 | In general, 4 different coordinate systems are used with 3 of them are described 11 | in https://github.com/mcordts/cityscapesScripts/blob/master/docs/csCalibration.pdf 12 | 1. The vehicle coordinate system V according to ISO 8855 with the origin 13 | on the ground below of the rear axis center, x pointing in driving direction, 14 | y pointing left, and z pointing up. 15 | 2. The camera coordinate system C with the origin in the camera’s optical 16 | center and same orientation as V. 17 | 3. The image coordinate system I with the origin in the top-left image pixel, 18 | u pointing right, and v pointing down. 19 | 4. In addition, we also add the coordinate system S with the same origin as C, 20 | but the orientation of I, ie. x pointing right, y down, and z in the 21 | driving direction. 22 | 23 | All GT annotations are given in the ISO coordinate system V and hence, the 24 | evaluation requires the data to be available in this coordinate system. 25 | 26 | For V and C it is: For S and I it is: 27 | 28 | ^ ^ 29 | z | ^ / z/d 30 | | / x / 31 | | / / 32 | | / +------------> 33 | |/ | x/u 34 | <------------+ | 35 | y | 36 | | y/v 37 | V 38 | """ 39 | 40 | 41 | # Define different coordinate systems 42 | CRS_V = 0 43 | CRS_C = 1 44 | CRS_S = 2 45 | 46 | 47 | def get_K_multiplier(): 48 | K_multiplier = np.zeros((3, 3)) 49 | K_multiplier[0][1] = K_multiplier[1][2] = -1 50 | K_multiplier[2][0] = 1 51 | return K_multiplier 52 | 53 | 54 | def get_projection_matrix(camera): 55 | K_matrix = np.zeros((3, 3)) 56 | K_matrix[0][0] = camera.fx 57 | K_matrix[0][2] = camera.u0 58 | K_matrix[1][1] = camera.fy 59 | K_matrix[1][2] = camera.v0 60 | K_matrix[2][2] = 1 61 | return K_matrix 62 | 63 | 64 | def apply_transformation_points(points, transformation_matrix): 65 | points = np.concatenate([points, np.ones((points.shape[0], 1))], axis=1) 66 | points = np.matmul(transformation_matrix, points.T).T 67 | return points 68 | 69 | 70 | class Camera(object): 71 | def __init__( 72 | self, 73 | fx, 74 | fy, 75 | u0, 76 | v0, 77 | sensor_T_ISO_8855, 78 | imgWidth=2048, 79 | imgHeight=1024): 80 | self.fx = fx 81 | self.fy = fy 82 | self.u0 = u0 83 | self.v0 = v0 84 | self.sensor_T_ISO_8855 = sensor_T_ISO_8855 85 | self.imgWidth = imgWidth 86 | self.imgHeight = imgHeight 87 | 88 | 89 | class Box3dImageTransform(object): 90 | def __init__(self, camera): 91 | self._camera = camera 92 | self._rotation_matrix = np.zeros((3, 3)) 93 | self._size = np.zeros((3,)) 94 | self._center = np.zeros((3,)) 95 | 96 | self.loc = ["BLB", "BRB", "FRB", "FLB", "BLT", "BRT", "FRT", "FLT"] 97 | 98 | self._box_points_2d = np.zeros((8, 2)) 99 | self._box_points_3d_vehicle = np.zeros((8, 3)) 100 | self._box_points_3d_cam = np.zeros((8, 3)) 101 | 102 | self.bottom_arrow_2d = np.zeros((2, 2)) 103 | self._bottom_arrow_3d_vehicle = np.zeros((2, 3)) 104 | self._bottom_arrow_3d_cam = np.zeros((2, 3)) 105 | 106 | self._box_left_side_cropped_2d = [] 107 | self._box_right_side_cropped_2d = [] 108 | self._box_front_side_cropped_2d = [] 109 | self._box_back_side_cropped_2d = [] 110 | self._box_top_side_cropped_2d = [] 111 | self._box_bottom_side_cropped_2d = [] 112 | 113 | def initialize_box_from_annotation(self, csBbox3dAnnotation, coordinate_system=CRS_V): 114 | # Unpack annotation and call initialize_box() method 115 | self.initialize_box( 116 | csBbox3dAnnotation.dims, 117 | csBbox3dAnnotation.rotation, 118 | csBbox3dAnnotation.center, 119 | coordinate_system=coordinate_system 120 | ) 121 | 122 | def initialize_box(self, size, quaternion, center, coordinate_system=CRS_V): 123 | # Internally, the box is always stored in the ISO 8855 coordinate system V 124 | # If the box is passed with another coordinate system, we transform it to V first. 125 | # "size" is always given in LxWxH 126 | K_multiplier = get_K_multiplier() 127 | quaternion_rot = Quaternion(quaternion) 128 | center = np.array(center) 129 | 130 | if coordinate_system == CRS_S: # convert it to CRS_C first 131 | center = np.matmul(K_multiplier.T, center.T).T 132 | image_T_sensor_quaternion = Quaternion(matrix=K_multiplier) 133 | quaternion_rot = ( 134 | image_T_sensor_quaternion.inverse * 135 | quaternion_rot * 136 | image_T_sensor_quaternion 137 | ) 138 | 139 | # center and quaternion must be corrected 140 | if coordinate_system == CRS_C or coordinate_system == CRS_S: 141 | sensor_T_ISO_8855_4x4 = np.eye(4) 142 | sensor_T_ISO_8855_4x4[:3, :] = np.array(self._camera.sensor_T_ISO_8855) 143 | sensor_T_ISO_8855_4x4_inv = np.linalg.inv(sensor_T_ISO_8855_4x4) 144 | center_T = np.ones((4, 1)) 145 | center_T[:3, 0] = center.T 146 | center = np.matmul(sensor_T_ISO_8855_4x4_inv, center_T) 147 | center = (center.T)[0, :3] 148 | 149 | sensor_T_ISO_8855_quaternion = Quaternion( 150 | matrix=np.array(self._camera.sensor_T_ISO_8855)[:3, :3]) 151 | quaternion_rot = sensor_T_ISO_8855_quaternion.inverse * quaternion_rot 152 | 153 | self._size = np.array(size) 154 | self._rotation_matrix = np.array(quaternion_rot.rotation_matrix) 155 | self._center = center 156 | 157 | self.update() 158 | 159 | def get_vertices(self, coordinate_system=CRS_V): 160 | if coordinate_system == CRS_V: 161 | box_points_3d = self._box_points_3d_vehicle 162 | 163 | if coordinate_system == CRS_C or coordinate_system == CRS_S: 164 | box_points_3d = apply_transformation_points( 165 | self._box_points_3d_vehicle, self._camera.sensor_T_ISO_8855 166 | ) 167 | 168 | if coordinate_system == CRS_S: 169 | K_multiplier = get_K_multiplier() 170 | box_points_3d = np.matmul(K_multiplier, box_points_3d.T).T 171 | 172 | return {l: p for (l, p) in zip(self.loc, box_points_3d)} 173 | 174 | def get_vertices_2d(self): 175 | return {l: p for (l, p) in zip(self.loc, self._box_points_2d)} 176 | 177 | def get_parameters(self, coordinate_system=CRS_V): 178 | K_multiplier = get_K_multiplier() 179 | quaternion_rot = Quaternion(matrix=self._rotation_matrix) 180 | center = self._center 181 | 182 | # center and quaternion must be corrected 183 | if coordinate_system == CRS_C or coordinate_system == CRS_S: 184 | sensor_T_ISO_8855_4x4 = np.eye(4) 185 | sensor_T_ISO_8855_4x4[:3, :] = np.array(self._camera.sensor_T_ISO_8855) 186 | center_T = np.ones((4, 1)) 187 | center_T[:3, 0] = center.T 188 | center = np.matmul(sensor_T_ISO_8855_4x4, center_T) 189 | center = (center.T)[0, :3] 190 | sensor_T_ISO_8855_quaternion = Quaternion( 191 | matrix=np.array(self._camera.sensor_T_ISO_8855)[:3, :3] 192 | ) 193 | quaternion_rot = sensor_T_ISO_8855_quaternion * quaternion_rot 194 | 195 | # change axis 196 | if coordinate_system == CRS_S: 197 | center = np.matmul(K_multiplier, center.T).T 198 | image_T_sensor_quaternion = Quaternion(matrix=K_multiplier) 199 | quaternion_rot = ( 200 | image_T_sensor_quaternion * 201 | quaternion_rot * 202 | image_T_sensor_quaternion.inverse 203 | ) 204 | 205 | return (self._size, center, quaternion_rot) 206 | 207 | def _get_side_visibility(self, face_center, face_normal): 208 | return np.dot(face_normal, face_center) < 0 209 | 210 | def get_all_side_visibilities(self): 211 | K_multiplier = get_K_multiplier() 212 | rotation_matrix_cam = np.matmul( 213 | np.matmul(K_multiplier, self._rotation_matrix), K_multiplier.T 214 | ) 215 | 216 | box_vector_x = rotation_matrix_cam[:, 0] 217 | box_vector_y = rotation_matrix_cam[:, 1] 218 | box_vector_z = rotation_matrix_cam[:, 2] 219 | 220 | front_visible = self._get_side_visibility( 221 | (self._box_points_3d_cam[3] + self._box_points_3d_cam[6]) / 2, box_vector_z 222 | ) 223 | back_visible = self._get_side_visibility( 224 | (self._box_points_3d_cam[0] + self._box_points_3d_cam[5]) / 2, -box_vector_z 225 | ) 226 | top_visible = self._get_side_visibility( 227 | (self._box_points_3d_cam[7] + self._box_points_3d_cam[5]) / 2, -box_vector_y 228 | ) 229 | bottom_visible = self._get_side_visibility( 230 | (self._box_points_3d_cam[0] + self._box_points_3d_cam[2]) / 2, box_vector_y 231 | ) 232 | left_visible = self._get_side_visibility( 233 | (self._box_points_3d_cam[0] + self._box_points_3d_cam[7]) / 2, -box_vector_x 234 | ) 235 | right_visible = self._get_side_visibility( 236 | (self._box_points_3d_cam[1] + self._box_points_3d_cam[6]) / 2, box_vector_x 237 | ) 238 | 239 | return [ 240 | front_visible, 241 | back_visible, 242 | top_visible, 243 | bottom_visible, 244 | left_visible, 245 | right_visible, 246 | ] 247 | 248 | def get_all_side_polygons_2d(self): 249 | front_side = self._box_front_side_cropped_2d 250 | back_side = self._box_back_side_cropped_2d 251 | top_side = self._box_top_side_cropped_2d 252 | bottom_side = self._box_bottom_side_cropped_2d 253 | left_side = self._box_left_side_cropped_2d 254 | right_side = self._box_right_side_cropped_2d 255 | 256 | return [front_side, back_side, top_side, bottom_side, left_side, right_side] 257 | 258 | def get_amodal_box_2d(self): 259 | xs = [] 260 | ys = [] 261 | 262 | for side_polygon in self.get_all_side_polygons_2d(): 263 | for [x, y] in side_polygon: 264 | xs.append(x) 265 | ys.append(y) 266 | 267 | # if the whole box is behind the camera, return [0., 0., 0., 0.] 268 | if len(xs) == 0: 269 | return [0., 0., 0., 0.] 270 | 271 | return [ 272 | min(self._camera.imgWidth - 1, max(0, min(xs))), 273 | min(self._camera.imgHeight - 1, max(0, min(ys))), 274 | min(self._camera.imgWidth - 1, max(0, max(xs))), 275 | min(self._camera.imgHeight - 1, max(0, max(ys))) 276 | ] 277 | 278 | def _crop_side_polygon_and_project(self, side_point_indices=[], side_points=[]): 279 | K_matrix = get_projection_matrix(self._camera) 280 | camera_plane_z = 0.01 281 | 282 | side_points_3d_cam = [self._box_points_3d_cam[i] for i in side_point_indices] 283 | side_points_3d_cam += side_points 284 | 285 | cropped_polygon_3d = [] 286 | for i, point in enumerate(side_points_3d_cam): 287 | if point[2] > camera_plane_z: # 1 cm 288 | cropped_polygon_3d.append(point) 289 | else: 290 | next_index = (i + 1) % len(side_points_3d_cam) 291 | prev_index = i - 1 292 | 293 | if side_points_3d_cam[prev_index][2] > camera_plane_z: 294 | delta_0 = point - side_points_3d_cam[prev_index] 295 | k_0 = (camera_plane_z - point[2]) / delta_0[2] 296 | point_0 = point + k_0 * delta_0 297 | cropped_polygon_3d.append(point_0) 298 | 299 | if side_points_3d_cam[next_index][2] > camera_plane_z: 300 | delta_1 = point - side_points_3d_cam[next_index] 301 | k_1 = (camera_plane_z - point[2]) / delta_1[2] 302 | point_1 = point + k_1 * delta_1 303 | cropped_polygon_3d.append(point_1) 304 | 305 | if len(cropped_polygon_3d) == 0: 306 | cropped_polygon_2d = [] 307 | else: 308 | cropped_polygon_2d = np.matmul(K_matrix, np.array(cropped_polygon_3d).T) 309 | cropped_polygon_2d = cropped_polygon_2d[:2, :] / cropped_polygon_2d[-1, :] 310 | cropped_polygon_2d = cropped_polygon_2d.T.tolist() 311 | cropped_polygon_2d.append(cropped_polygon_2d[0]) 312 | 313 | return cropped_polygon_2d 314 | 315 | def update(self): 316 | self._update_box_points_3d() 317 | self._update_box_sides_cropped() 318 | self._update_box_points_2d() 319 | 320 | def _update_box_sides_cropped(self): 321 | self._box_left_side_cropped_2d = self._crop_side_polygon_and_project( 322 | [3, 0, 4, 7] 323 | ) 324 | self._box_right_side_cropped_2d = self._crop_side_polygon_and_project( 325 | [1, 5, 6, 2] 326 | ) 327 | self._box_front_side_cropped_2d = self._crop_side_polygon_and_project( 328 | [3, 2, 6, 7] 329 | ) 330 | self._box_back_side_cropped_2d = self._crop_side_polygon_and_project( 331 | [0, 1, 5, 4] 332 | ) 333 | self._box_top_side_cropped_2d = self._crop_side_polygon_and_project( 334 | [4, 5, 6, 7] 335 | ) 336 | self._box_bottom_side_cropped_2d = self._crop_side_polygon_and_project( 337 | [0, 1, 2, 3] 338 | ) 339 | self.bottom_arrow_2d = self._crop_side_polygon_and_project( 340 | side_points=[self._bottom_arrow_3d_cam[x] for x in range(2)] 341 | ) 342 | 343 | def _update_box_points_3d(self): 344 | center_vectors = np.zeros((8, 3)) 345 | # Bottom Face 346 | center_vectors[0] = np.array( 347 | [-self._size[0] / 2, self._size[1] / 2, -self._size[2] / 2] 348 | # Back Left Bottom 349 | ) 350 | center_vectors[1] = np.array( 351 | [-self._size[0] / 2, -self._size[1] / 2, -self._size[2] / 2] 352 | # Back Right Bottom 353 | ) 354 | center_vectors[2] = np.array( 355 | [self._size[0] / 2, -self._size[1] / 2, -self._size[2] / 2] 356 | # Front Right Bottom 357 | ) 358 | center_vectors[3] = np.array( 359 | [self._size[0] / 2, self._size[1] / 2, -self._size[2] / 2] 360 | # Front Left Bottom 361 | ) 362 | 363 | # Top Face 364 | center_vectors[4] = np.array( 365 | [-self._size[0] / 2, self._size[1] / 2, self._size[2] / 2] 366 | # Back Left Top 367 | ) 368 | center_vectors[5] = np.array( 369 | [-self._size[0] / 2, -self._size[1] / 2, self._size[2] / 2] 370 | # Back Right Top 371 | ) 372 | center_vectors[6] = np.array( 373 | [self._size[0] / 2, -self._size[1] / 2, self._size[2] / 2] 374 | # Front Right Top 375 | ) 376 | center_vectors[7] = np.array( 377 | [self._size[0] / 2, self._size[1] / 2, self._size[2] / 2] 378 | # Front Left Top 379 | ) 380 | 381 | # Rotate the vectors 382 | box_points_3d = np.matmul(self._rotation_matrix, center_vectors.T).T 383 | # Translate to box position in 3d space 384 | box_points_3d += self._center 385 | 386 | self._box_points_3d_vehicle = box_points_3d 387 | 388 | self._bottom_arrow_3d_vehicle = np.array( 389 | [ 390 | (0.5 * (self._box_points_3d_vehicle[3] + self._box_points_3d_vehicle[2])), 391 | (0.5 * (self._box_points_3d_vehicle[3] + self._box_points_3d_vehicle[1])), 392 | ] 393 | ) 394 | bottom_arrow_3d_cam = apply_transformation_points( 395 | self._bottom_arrow_3d_vehicle, self._camera.sensor_T_ISO_8855 396 | ) 397 | 398 | # Points in ISO8855 system with origin at the sensor 399 | box_points_3d_cam = apply_transformation_points( 400 | self._box_points_3d_vehicle, self._camera.sensor_T_ISO_8855 401 | ) 402 | K_multiplier = get_K_multiplier() 403 | self._box_points_3d_cam = np.matmul(K_multiplier, box_points_3d_cam.T).T 404 | self._bottom_arrow_3d_cam = np.matmul(K_multiplier, bottom_arrow_3d_cam.T).T 405 | 406 | def _update_box_points_2d(self): 407 | K_matrix = get_projection_matrix(self._camera) 408 | box_points_2d = np.matmul(K_matrix, self._box_points_3d_cam.T) 409 | box_points_2d = box_points_2d[:2, :] / box_points_2d[-1, :] 410 | self._box_points_2d = box_points_2d.T 411 | -------------------------------------------------------------------------------- /cityscapesscripts/evaluation/plot3dResults.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import os 5 | import json 6 | from typing import ( 7 | List, 8 | Tuple 9 | ) 10 | import numpy as np 11 | 12 | import matplotlib.pyplot as plt 13 | from matplotlib.axes import Axes 14 | 15 | from cityscapesscripts.helpers.labels import name2label 16 | 17 | 18 | def csToMplColor(label): 19 | color = name2label[label].color 20 | return [x/255. for x in color] 21 | 22 | 23 | def create_table_row( 24 | axis, # type: Axes 25 | x_pos, # type: float 26 | y_pos, # type: float 27 | data_dict, # type: dict 28 | title, # type: str 29 | key, # type: str 30 | subdict_key=None # type: str 31 | ): 32 | # type: (...) -> None 33 | """Creates a row presenting scores for all classes in category ``key`` in ``data_dict``. 34 | 35 | Args: 36 | axis (Axes): Axes-instances to use for the subplot 37 | x_pos (float): x-value for the left of the row relative in the given subplot 38 | y_pos (float): y-value for the top of the row relative in the given subplot 39 | data_dict (dict): dict containing data to visualize 40 | title (str): title of the row / category-name 41 | key (str): key in ``data_dict`` to obtain data to visualize 42 | subdict_key (str or None): 43 | additional key to access data, if ``data_dict[key]`` returns again a dict, 44 | otherwise None 45 | """ 46 | 47 | axis.text(x_pos, y_pos, title, fontdict={'weight': 'bold'}) 48 | y_pos -= 0.1 49 | delta_x_pos = 0.2 50 | 51 | for (cat, valdict) in data_dict[key].items(): 52 | val = valdict if subdict_key is None else valdict[subdict_key] 53 | axis.text(x_pos, y_pos, cat) 54 | axis.text(x_pos+delta_x_pos, y_pos, "{:.4f}".format(val * 100), ha="right") 55 | y_pos -= 0.1 56 | 57 | # add Mean 58 | y_pos -= 0.05 59 | axis.text(x_pos, y_pos, "Mean", fontdict={'weight': 'bold'}) 60 | axis.text(x_pos+delta_x_pos, y_pos, "{:.4f}".format( 61 | data_dict["m"+key] * 100), fontdict={'weight': 'bold'}, ha="right") 62 | 63 | 64 | def create_result_table_and_legend_plot( 65 | axis, # type: Axes 66 | data_to_plot, # type: dict 67 | handles_labels # type: Tuple[List, List] 68 | ): 69 | # type: (...) -> None 70 | """Creates the plot-section containing a table with result scores and labels. 71 | 72 | Args: 73 | axis (Axes): Axes-instances to use for the subplot 74 | data_to_plot (dict): Dictionary containing ``"Detection_Score"`` and ``"AP"`` 75 | and corresponding mean values 76 | handles_labels (Tuple[List, List]): Tuple of matplotlib handles and corresponding labels 77 | """ 78 | 79 | required_keys = ["Detection_Score", "mDetection_Score", "AP", "mAP"] 80 | assert all([key in data_to_plot.keys() for key in required_keys]) 81 | 82 | # Results 83 | axis.axis("off") 84 | axis.text(0, 0.95, 'Results', fontdict={'weight': 'bold', 'size': 16}) 85 | 86 | y_pos_row = 0.75 87 | # 2D AP results 88 | create_table_row(axis, 0.00, y_pos_row, data_to_plot, 89 | title="2D AP", key="AP", subdict_key="auc") 90 | 91 | # Detection score results 92 | create_table_row(axis, 0.28, y_pos_row, data_to_plot, 93 | title="Detection Score", key="Detection_Score", subdict_key=None) 94 | 95 | # Legend 96 | x_pos_legend = 0.6 97 | y_pos_legend = 0.75 98 | y_pos_dot_size = 0.0 99 | axis.text(x_pos_legend, 0.95, 'Legend', 100 | fontdict={'weight': 'bold', 'size': 16}) 101 | axis.legend(*handles_labels, frameon=True, 102 | loc="upper left", bbox_to_anchor=(x_pos_legend, y_pos_legend), ncol=2) 103 | 104 | # add data-point-marker size explanation 105 | dot_size_explanation = "The size of each data-point-marker indicates\n" 106 | dot_size_explanation += "the relative amount of samples for that data-\n" 107 | dot_size_explanation += "point, with large dots indicate larger samples." 108 | axis.text(x_pos_legend, y_pos_dot_size, dot_size_explanation) 109 | 110 | 111 | def create_spider_chart_plot( 112 | axis, # type: Axes 113 | data_to_plot, # type: dict 114 | categories, # type: List[str] 115 | accept_classes # type: List[str] 116 | ): 117 | # type: (...) -> None 118 | """Creates spider-chart with ``categories`` for all classes in ``accept_classes``. 119 | 120 | Args: 121 | axis (Axes): Axes-instances to use for the spider-chart 122 | data_to_plot (dict): Dictionary containing ``categories`` as keys. 123 | categories (list of str): List of category-names to use for the spider-chart. 124 | accept_classes (list of str): List of class-names to use for the spider-chart. 125 | """ 126 | 127 | # create labels 128 | lables = [category.replace("_", "-") for category in categories] 129 | 130 | # Calculate metrics for each class 131 | vals = { 132 | cat: [cat_vals["auc"] 133 | for x, cat_vals in data_to_plot[cat].items() if x in accept_classes] 134 | for cat in categories 135 | } 136 | 137 | # norm everything to AP 138 | for key in ["Center_Dist", "Size_Similarity", "OS_Yaw", "OS_Pitch_Roll"]: 139 | vals[key] = [v * float(ap) for (v, ap) in zip(vals[key], vals["AP"])] 140 | 141 | # setup axis 142 | num_categories = len(categories) 143 | 144 | angles = [n / float(num_categories) * 2 * 145 | np.pi for n in range(num_categories)] 146 | angles += angles[:1] 147 | 148 | axis.set_theta_offset(np.pi / 2.) 149 | axis.set_theta_direction(-1) 150 | axis.set_rlabel_position(0) 151 | axis.set_yticks([0.25, 0.50, 0.75]) 152 | axis.set_yticklabels(["0.25", "0.50", "0.75"], color="grey", size=7) 153 | axis.tick_params(axis="x", direction="out", pad=10) 154 | axis.set_ylim([0, 1]) 155 | axis.set_xticks(np.arange(0, 2.0*np.pi, np.pi/2.5)) 156 | axis.set_xticklabels(lables) 157 | 158 | for idx, label in enumerate(accept_classes): 159 | values = [x[idx] for x in [vals[cat] for cat in categories]] 160 | values += values[:1] 161 | 162 | axis.plot(angles, values, linewidth=1, 163 | linestyle='solid', color=csToMplColor(label)) 164 | axis.fill( 165 | angles, values, color=csToMplColor(label), alpha=0.05) 166 | 167 | axis.plot(angles, [np.mean(x) for x in [vals[cat] for cat in categories] + [ 168 | vals["AP"]]], linewidth=1, linestyle='solid', color="r", label="Mean") 169 | axis.legend(bbox_to_anchor=(0, 0)) 170 | 171 | 172 | def create_AP_plot( 173 | axis, # type: Axes 174 | data_to_plot, # type: dict 175 | accept_classes, # type: List[str] 176 | max_depth # type: int 177 | ): 178 | # type: (...) -> None 179 | """Create the average precision (AP) subplot for classes in ``accept_classes``. 180 | 181 | Args: 182 | axis (Axes): Axes-instances to use for AP-plot 183 | data_to_plot (dict): Dictionary containing data to be visualized 184 | for all classes in ``accept_classes`` 185 | accept_classes (list of str): List of class-names to use for the spider-chart 186 | max_depth (int): maximal encountered depth value 187 | """ 188 | 189 | if "AP_per_depth" not in data_to_plot: 190 | raise ValueError() 191 | 192 | axis.set_title("AP per depth") 193 | axis.set_ylim([0, 1.01]) 194 | axis.set_ylabel("AP") 195 | 196 | for label in accept_classes: 197 | aps = data_to_plot["AP_per_depth"][label] 198 | 199 | x_vals = [float(x) for x in list(aps.keys())] 200 | y_vals = [float(x["auc"]) for x in list(aps.values())] 201 | 202 | fill_standard_subplot(axis, x_vals, y_vals, label, [], max_depth) 203 | 204 | 205 | def set_up_xaxis( 206 | axis, # type: Axes 207 | max_depth, # type: int 208 | num_ticks # type: int 209 | ): 210 | # type: (...) -> None 211 | """Sets up the x-Axis of given Axes-instance ``axis``. 212 | 213 | Args: 214 | axis (Axes): Axes-instances to use 215 | max_depth (int): max value of the x-axis is set to ``max_depth+1`` 216 | num_ticks (int): number of ticks on the x-axis 217 | """ 218 | axis.set_xlim([0, max_depth]) 219 | axis.set_xticks(np.linspace(0, max_depth, num_ticks + 1)) 220 | axis.set_xticklabels(["{:.1f}".format(x) for x in np.linspace(0, max_depth, num_ticks + 1)]) 221 | 222 | 223 | def set_up_PR_plot_axis( 224 | axis, # type: Axes 225 | min_iou, # type: float 226 | matching_method # type: str 227 | ): 228 | # type: (...) -> None 229 | """Sets up the axis for the precision plot.""" 230 | axis.set_title("PR Curve@{:.2f} ({})".format(min_iou, matching_method)) 231 | axis.set_xlabel("Recall") 232 | axis.set_ylabel("Precision") 233 | axis.set_xlim([0, 1.0]) 234 | axis.set_ylim([0, 1.01]) 235 | axis.set_xticks(np.arange(0, 1.01, 0.1)) 236 | axis.set_xticklabels([x / 10. for x in range(11)]) 237 | 238 | 239 | def create_all_axes( 240 | max_depth, # type: int 241 | num_ticks # type: int 242 | ): 243 | # type: (...) -> None 244 | """Creates all Axes-instances of the 8 subplots. 245 | 246 | Args: 247 | max_depth (int): max value of the x-axis is set to ``max_depth+1`` 248 | num_ticks (int): number of ticks on the x-axis 249 | 250 | Returns: 251 | ax_results (Axes): Axes-instance of the subplot 252 | containing the results-table and plot-legend 253 | ax_spider (Axes): Axes-instance of the subplot 254 | containing the spider_chart of AP-values for 255 | axes (List[Axes]): 6 Axes-instances for the categories. 256 | """ 257 | 258 | ax_results = plt.subplot2grid((4, 2), (0, 0)) 259 | ax_spider = plt.subplot2grid((4, 2), (0, 1), polar=True) 260 | ax1 = plt.subplot2grid((4, 2), (1, 0)) 261 | ax2 = plt.subplot2grid((4, 2), (1, 1)) 262 | ax3 = plt.subplot2grid((4, 2), (2, 0), sharex=ax2) 263 | ax4 = plt.subplot2grid((4, 2), (2, 1), sharex=ax2) 264 | ax5 = plt.subplot2grid((4, 2), (3, 0), sharex=ax2) 265 | ax6 = plt.subplot2grid((4, 2), (3, 1), sharex=ax2) 266 | axes = (ax1, ax2, ax3, ax4, ax5, ax6) 267 | 268 | # set up x-axes for ax2-ax6 269 | set_up_xaxis(ax2, max_depth, num_ticks) 270 | ax5.set_xlabel("Depth [m]") 271 | ax6.set_xlabel("Depth [m]") 272 | 273 | return ax_results, ax_spider, axes 274 | 275 | 276 | def create_PR_plot( 277 | axis, # type: Axes 278 | data, # type: dict 279 | accept_classes # type: List[str] 280 | ): 281 | # type: (...) -> None 282 | """Fills precision-recall (PR) subplot with data and finalizes ``axis``-set-up. 283 | 284 | Args: 285 | axis (Axes): Axes-instance of the subplot 286 | data (dict): data-dictionnary containing precision and recall values 287 | for all classes in ``accept_classes`` 288 | accept_classes (list of str): 289 | """ 290 | set_up_PR_plot_axis( 291 | axis, 292 | data["eval_params"]["min_iou_to_match"], 293 | data["eval_params"]["matching_method"] 294 | ) 295 | 296 | for label in accept_classes: 297 | recalls_ = data['AP'][label]["data"]["recall"] 298 | precisions_ = data['AP'][label]["data"]["precision"] 299 | 300 | # sort the data ascending 301 | sorted_pairs = sorted( 302 | zip(recalls_, precisions_), key=lambda pair: pair[0]) 303 | recalls, precisions = map(list, zip(*sorted_pairs)) 304 | recalls = [0.] + recalls 305 | precisions = [0.] + precisions 306 | 307 | recalls += recalls[-1:] + [1.] 308 | precisions += [0., 0.] 309 | 310 | # precision values should be decreasing only 311 | # p(r) = max{r' > r} p(r') 312 | for i in range(len(precisions) - 2, -1, -1): 313 | precisions[i] = np.maximum(precisions[i], precisions[i + 1]) 314 | 315 | axis.plot(recalls, precisions, label=label, 316 | color=csToMplColor(label)) 317 | 318 | 319 | def fill_and_finalize_subplot( 320 | category, # type: str 321 | data_to_plot, # type: dict 322 | accept_classes, # type: List[str] 323 | axis, # type: Axes 324 | max_depth # type: int 325 | ): 326 | # type: (...) -> None 327 | """Plot data to subplots by selecting correct data for given ``category`` and looping over 328 | all classes in ``accept_classes``. 329 | 330 | Args: 331 | category (str): score category, one of 332 | ["PR", "AP", "Center_Dist", "Size_Similarity", "OS_Yaw", "OS_Pitch_Roll"] 333 | data_to_plot (dict): Dictionary containing data to be visualized. 334 | accept_classes (list of str): List of class-names to use for the spider-chart. 335 | axis (Axes): Axes-instances to use for the subplot 336 | max_depth (int): maximal encountered depth value 337 | """ 338 | 339 | if category == 'PR': 340 | create_PR_plot(axis, data_to_plot, accept_classes) 341 | 342 | elif category == 'AP': 343 | create_AP_plot(axis, data_to_plot, accept_classes, max_depth) 344 | 345 | elif category in ["Center_Dist", "Size_Similarity", "OS_Yaw", "OS_Pitch_Roll"]: 346 | 347 | axis.set_title(category.replace("_", " ") + " (DDTP Metric)") 348 | 349 | if category == 'Center_Dist': 350 | axis.set_ylim([0, 25]) 351 | axis.set_ylabel("Distance [m]") 352 | else: 353 | axis.set_ylim([0., 1.01]) 354 | axis.set_ylabel("Similarity") 355 | 356 | for label in accept_classes: 357 | x_vals, y_vals = get_x_y_vals( 358 | data_to_plot[category][label]["data"]) 359 | available_items_scaling = get_available_items_scaling( 360 | data_to_plot[category][label]["items"]) 361 | 362 | if category == 'Center_Dist': 363 | y_vals = [(1 - y) * max_depth for y in y_vals] 364 | 365 | fill_standard_subplot( 366 | axis, x_vals, y_vals, label, available_items_scaling, max_depth) 367 | 368 | else: 369 | raise ValueError("Unsupported category, got {}.".format(category)) 370 | 371 | 372 | def fill_standard_subplot( 373 | axis, # type: Axes 374 | x_vals_unsorted, # type: List[float] 375 | y_vals_unsorted, # type: List[float] 376 | label, # type: str 377 | available_items_scaling, # type: List[float] 378 | max_depth # type: int 379 | ): 380 | # type: (...) -> None 381 | """Fills standard-subplots with data for ``label`` with data. 382 | 383 | Includes scatter-plot with size-scaled data-points, line-plot and 384 | a dashed line from maximal value in ``x_vals`` to ``max_depth``. 385 | 386 | Args: 387 | axis (Axes): Axes-instances to use for the subplot 388 | x_vals (list of float): x-values to visualize 389 | y_vals (list of float): y-values to visualize 390 | label (str): name of class to visualize data for 391 | available_items_scaling (list of float): size of data-points 392 | max_depth (int): maximal value of x-axis 393 | """ 394 | 395 | sorted_pairs = sorted(zip(x_vals_unsorted, y_vals_unsorted), key=lambda x: x[0]) 396 | if len(sorted_pairs) > 0: 397 | x_vals, y_vals = map(list, zip(*sorted_pairs)) 398 | else: 399 | x_vals = x_vals_unsorted 400 | y_vals = y_vals_unsorted 401 | 402 | if len(available_items_scaling) > 0: 403 | axis.scatter(x_vals, y_vals, s=available_items_scaling, 404 | color=csToMplColor(label), marker="o", alpha=1.0) 405 | axis.plot(x_vals, y_vals, label=label, 406 | color=csToMplColor(label)) 407 | 408 | if len(x_vals) >= 1: 409 | axis.plot([x_vals[-1], max_depth], [y_vals[-1], y_vals[-1]], label=label, 410 | color=csToMplColor(label), linestyle="--", alpha=0.6) 411 | axis.plot([0, x_vals[0]], [y_vals[0], y_vals[0]], label=label, 412 | color=csToMplColor(label), linestyle="--", alpha=0.6) 413 | 414 | 415 | def get_available_items_scaling( 416 | data, # type: dict 417 | scale_fac=100 # type: float 418 | ): 419 | # type: (...) -> None 420 | """Counts available items per data-point. Normalizes and scales according to ``scale_fac``.""" 421 | available_items = list(data.values()) 422 | if len(available_items) == 0: 423 | return available_items 424 | 425 | max_num_item = max(available_items) 426 | available_items_scaling = [ 427 | x / float(max_num_item) * scale_fac for x in available_items] 428 | return available_items_scaling 429 | 430 | 431 | def get_x_y_vals( 432 | data # type: dict 433 | ): 434 | # type: (...) -> None 435 | """Reads and returns x- and y-values from dict.""" 436 | x_vals = [float(x) for x in list(data.keys())] 437 | y_vals = list(data.values()) 438 | return x_vals, y_vals 439 | 440 | 441 | def plot_data( 442 | data_to_plot # type: dict 443 | ): 444 | # type: (...) -> None 445 | """Creates the visualization of the data in ``data_to_plot``. 446 | 447 | Args: 448 | data_to_plot (dict): Dictionary containing data to be visualized. 449 | Has to contain the keys "AP", "Center_Dist", "Size_Similarity", 450 | "OS_Yaw", "OS_Pitch_Roll". 451 | """ 452 | 453 | # get max depth 454 | max_depth = data_to_plot["eval_params"]["max_depth"] 455 | 456 | # setup all categories 457 | categories = ["AP", "Center_Dist", "Size_Similarity", 458 | "OS_Yaw", "OS_Pitch_Roll"] 459 | subplot_categories = ["PR"] + categories 460 | assert all([key in data_to_plot.keys() for key in categories]) 461 | 462 | accept_classes = data_to_plot["eval_params"]["labels"] 463 | 464 | plt.figure(figsize=(20, 12), dpi=100) 465 | 466 | # create subplot-axes 467 | ax_results, ax_spider, axes = create_all_axes(max_depth, 10) 468 | 469 | # 1st fill subplots (3-8) 470 | for idx, category in enumerate(subplot_categories): 471 | fill_and_finalize_subplot( 472 | category, data_to_plot, accept_classes, axes[idx], max_depth) 473 | 474 | # 2nd plot Spider plot 475 | create_spider_chart_plot(ax_spider, data_to_plot, 476 | categories, accept_classes) 477 | 478 | # 3rd create subplot showing the table with result scores and labels 479 | create_result_table_and_legend_plot( 480 | ax_results, data_to_plot, axes[0].get_legend_handles_labels()) 481 | 482 | plt.tight_layout() 483 | # plt.savefig("results.pdf") 484 | plt.show() 485 | 486 | 487 | def prepare_data( 488 | json_path # type: str 489 | ): 490 | # type: (...) -> dict 491 | """Loads data from json-file. 492 | 493 | Args: 494 | json_path (str): Path to json-file from which data should be loaded 495 | """ 496 | 497 | with open(json_path) as file_: 498 | data = json.load(file_) 499 | 500 | return data 501 | 502 | 503 | def main(): 504 | parser = argparse.ArgumentParser() 505 | parser.add_argument("path", 506 | help='Path to result .json file as produced by 3D evaluation script. ' 507 | 'Can be downloaded from the evaluation server for test set results.') 508 | args = parser.parse_args() 509 | 510 | if not os.path.exists(args.path): 511 | raise Exception("Result file not found!") 512 | 513 | data = prepare_data(args.path) 514 | plot_data(data) 515 | 516 | 517 | if __name__ == "__main__": 518 | # call the main method 519 | main() 520 | -------------------------------------------------------------------------------- /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 | allResultsDict = createResultDict( confMatrix, classScoreList, classInstScoreList, categoryScoreList, categoryInstScoreList, perImageStats, args ) 541 | # write result file 542 | if args.JSONOutput: 543 | writeJSONFile( allResultsDict, args) 544 | 545 | # return confusion matrix 546 | return allResultsDict 547 | 548 | # Main evaluation method. Evaluates pairs of prediction and ground truth 549 | # images which are passed as arguments. 550 | def evaluatePair(predictionImgFileName, groundTruthImgFileName, confMatrix, instanceStats, perImageStats, args): 551 | # Loading all resources for evaluation. 552 | try: 553 | predictionImg = Image.open(predictionImgFileName) 554 | predictionNp = np.array(predictionImg) 555 | except: 556 | printError("Unable to load " + predictionImgFileName) 557 | try: 558 | groundTruthImg = Image.open(groundTruthImgFileName) 559 | groundTruthNp = np.array(groundTruthImg) 560 | except: 561 | printError("Unable to load " + groundTruthImgFileName) 562 | # load ground truth instances, if needed 563 | if args.evalInstLevelScore: 564 | groundTruthInstanceImgFileName = groundTruthImgFileName.replace("labelIds","instanceIds") 565 | try: 566 | instanceImg = Image.open(groundTruthInstanceImgFileName) 567 | instanceNp = np.array(instanceImg) 568 | except: 569 | printError("Unable to load " + groundTruthInstanceImgFileName) 570 | 571 | # Check for equal image sizes 572 | if (predictionImg.size[0] != groundTruthImg.size[0]): 573 | printError("Image widths of " + predictionImgFileName + " and " + groundTruthImgFileName + " are not equal.") 574 | if (predictionImg.size[1] != groundTruthImg.size[1]): 575 | printError("Image heights of " + predictionImgFileName + " and " + groundTruthImgFileName + " are not equal.") 576 | if ( len(predictionNp.shape) != 2 ): 577 | printError("Predicted image has multiple channels.") 578 | 579 | imgWidth = predictionImg.size[0] 580 | imgHeight = predictionImg.size[1] 581 | nbPixels = imgWidth*imgHeight 582 | 583 | # Evaluate images 584 | if (CSUPPORT): 585 | # using cython 586 | confMatrix = addToConfusionMatrix.cEvaluatePair(predictionNp, groundTruthNp, confMatrix, args.evalLabels) 587 | else: 588 | # the slower python way 589 | encoding_value = max(groundTruthNp.max(), predictionNp.max()).astype(np.int32) + 1 590 | encoded = (groundTruthNp.astype(np.int32) * encoding_value) + predictionNp 591 | 592 | values, cnt = np.unique(encoded, return_counts=True) 593 | 594 | for value, c in zip(values, cnt): 595 | pred_id = value % encoding_value 596 | gt_id = int((value - pred_id)/encoding_value) 597 | if not gt_id in args.evalLabels: 598 | printError("Unknown label with id {:}".format(gt_id)) 599 | confMatrix[gt_id][pred_id] += c 600 | 601 | 602 | if args.evalInstLevelScore: 603 | # Generate category masks 604 | categoryMasks = {} 605 | for category in instanceStats["categories"]: 606 | categoryMasks[category] = np.in1d( predictionNp , instanceStats["categories"][category]["labelIds"] ).reshape(predictionNp.shape) 607 | 608 | instList = np.unique(instanceNp[instanceNp > 1000]) 609 | for instId in instList: 610 | labelId = int(instId/1000) 611 | label = id2label[ labelId ] 612 | if label.ignoreInEval: 613 | continue 614 | 615 | mask = instanceNp==instId 616 | instSize = np.count_nonzero( mask ) 617 | 618 | tp = np.count_nonzero( predictionNp[mask] == labelId ) 619 | fn = instSize - tp 620 | 621 | weight = args.avgClassSize[label.name] / float(instSize) 622 | tpWeighted = float(tp) * weight 623 | fnWeighted = float(fn) * weight 624 | 625 | instanceStats["classes"][label.name]["tp"] += tp 626 | instanceStats["classes"][label.name]["fn"] += fn 627 | instanceStats["classes"][label.name]["tpWeighted"] += tpWeighted 628 | instanceStats["classes"][label.name]["fnWeighted"] += fnWeighted 629 | 630 | category = label.category 631 | if category in instanceStats["categories"]: 632 | catTp = 0 633 | catTp = np.count_nonzero( np.logical_and( mask , categoryMasks[category] ) ) 634 | catFn = instSize - catTp 635 | 636 | catTpWeighted = float(catTp) * weight 637 | catFnWeighted = float(catFn) * weight 638 | 639 | instanceStats["categories"][category]["tp"] += catTp 640 | instanceStats["categories"][category]["fn"] += catFn 641 | instanceStats["categories"][category]["tpWeighted"] += catTpWeighted 642 | instanceStats["categories"][category]["fnWeighted"] += catFnWeighted 643 | 644 | if args.evalPixelAccuracy: 645 | notIgnoredLabels = [l for l in args.evalLabels if not id2label[l].ignoreInEval] 646 | notIgnoredPixels = np.in1d( groundTruthNp , notIgnoredLabels , invert=True ).reshape(groundTruthNp.shape) 647 | erroneousPixels = np.logical_and( notIgnoredPixels , ( predictionNp != groundTruthNp ) ) 648 | perImageStats[predictionImgFileName] = {} 649 | perImageStats[predictionImgFileName]["nbNotIgnoredPixels"] = np.count_nonzero(notIgnoredPixels) 650 | perImageStats[predictionImgFileName]["nbCorrectPixels"] = np.count_nonzero(erroneousPixels) 651 | 652 | return nbPixels 653 | 654 | # The main method 655 | def main(): 656 | global args 657 | argv = sys.argv[1:] 658 | 659 | predictionImgList = [] 660 | groundTruthImgList = [] 661 | 662 | # the image lists can either be provided as arguments 663 | if (len(argv) > 3): 664 | for arg in argv: 665 | if ("gt" in arg or "groundtruth" in arg): 666 | groundTruthImgList.append(arg) 667 | elif ("pred" in arg): 668 | predictionImgList.append(arg) 669 | # however the no-argument way is prefered 670 | elif len(argv) == 0: 671 | # use the ground truth search string specified above 672 | groundTruthImgList = glob.glob(args.groundTruthSearch) 673 | if not groundTruthImgList: 674 | printError("Cannot find any ground truth images to use for evaluation. Searched for: {}".format(args.groundTruthSearch)) 675 | # get the corresponding prediction for each ground truth imag 676 | for gt in groundTruthImgList: 677 | predictionImgList.append( getPrediction(args,gt) ) 678 | 679 | # evaluate 680 | evaluateImgLists(predictionImgList, groundTruthImgList, args) 681 | 682 | return 683 | 684 | # call the main method 685 | if __name__ == "__main__": 686 | main() 687 | --------------------------------------------------------------------------------