├── .gitignore ├── LICENSE ├── README.md ├── attribute_datasets.py ├── make_up_predictions.py ├── pUtils ├── __init__.py ├── dump_args.py ├── fs.py ├── git.py └── user_query.py ├── parse_evaluation.py ├── pmetrics.py ├── preprocess_dataset.py ├── requirements.txt ├── visualize_crops.py └── visualize_pid.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Patrick Sudowe, Lehr- und Forschungsgebiet Informatik 8 (Computer Vision) RWTH Aachen University. 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parse27k_tools 2 | Tools for the Parse-27k Dataset - evaluation routines and some simple scripts to get started... 3 | 4 | Find the dataset here: 5 | [http://www.vision.rwth-aachen.de/parse27k] 6 | 7 | ## Installation & Requirements 8 | We use Python 2.7 on a 64-bit Linux -- but the tools should run on other platforms as well. 9 | 10 | The tools have some dependencies. If you are using Python regularly for scientific computing, 11 | all of this is likely installed already. Otherwise they can all be easily obtained through `pip`. 12 | These tools depend (directly or indirectly) on: 13 | ``` 14 | futures 15 | progressbar2 16 | h5py 17 | Pillow 18 | scipy 19 | scikit-image 20 | matplotlib 21 | ``` 22 | Again, if you have a working Python working environment, most of these will already be installed. 23 | 24 | 25 | #### Using this in case you have no Python Environment 26 | 27 | If you have never used Python before, install Python 2.7 on your system through your favorite installation method (e.g. `apt-get`). 28 | I would then recommend to setup a `virtualenv` with the dependencies. For this, follow these commands: 29 | ``` 30 | virtualenv new_environment 31 | source new_environment/bin/activate 32 | pip install numpy 33 | pip install scipy 34 | pip install scikit-image 35 | pip install -r requirements.txt 36 | ``` 37 | *Note* due to some build problems, which I only partially understand, I could not directly install 38 | all requirements with a single `pip` call. It did work, to first install `numpy`,`scipy`, and `scikit-image` separately. 39 | If anybody can point me to a nicer solution, I will be happy to adapt this. 40 | 41 | ## Usage 42 | You can run the scripts with option `-h` for a basic description of their parameters. 43 | The basic functions are: 44 | 45 | * `preprocess_dataset.py`: create crops from the full images and store in `HDF5` file. 46 | * `parse_evaluation.py`: compute performance measures from predictions. 47 | * `visualize_crops.py`: visualize examples from a file generated by `preprocess_dataset.py`. 48 | * `visualize_pid.py`: visualize a specific *pedestrianID* from an `HDF5` file generated by `preprocess_dataset.py`. 49 | The *pedestrianID* is a unique identifier for the examples within the dataset. 50 | 51 | ## Preprocessing the Dataset 52 | As an example of how to use the dataset, this package includes `preprocess_dataset.py`. 53 | This will read the annotations from the database, crop the examples from the original images, and 54 | store the result in an `HDF5` file. 55 | The resulting file will have two *datasets* (as it is called in `HDF5` language): `crops` and `labels`. 56 | The `labels` is a *NxK* matrix, for *N* examples and *K* attributes. 57 | The meaning is encoded in the `names` attribute. 58 | 59 | ``` 60 | $ export PARSE_PATH=_where_you_saved_the_dataset_ 61 | $ ./preprocess_dataset.py --output_dir /tmp/crops --padding_mode zero --padding 32 62 | $ ./visualize_crops.py -r /tmp/crops/train.hdf5 63 | ``` 64 | ## Evaluation Modes 65 | The tool `parse_evaluation.py` allows two modes of evaluation - *retrieval* and *classification*. 66 | See [our paper](http://www.vision.rwth-aachen.de/publications/pdf/parse-acn-iccv2015) for details on the difference. 67 | 68 | The tool expects the predictions in an `HDF5`-file, with one *dataset* for each attribute. 69 | You can use the `make_up_predictions.py` script to learn about the expected format. 70 | 71 | ``` 72 | $ ./parse_evaluation.py /tmp/crops/test.hdf5 ./random_predictions.hdf5 73 | ``` 74 | 75 | ## Bugs & Problems & Updates 76 | The tools here were taken from a larger set of tools as we prepared the dataset for publication. 77 | In case we notice problems or bugs introduced in this process, we will fix these here. 78 | 79 | If you are working with the *Parse-27k* dataset, we encourage you to *follow this repository*. 80 | In case there are any bug-fixes or changes to the evaluation pipeline, GitHub will let you know. 81 | 82 | -------------------------------------------------------------------------------- /attribute_datasets.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module provides iterator functions for the PARSE-27k dataset. 3 | """ 4 | import numpy as np 5 | import os, re 6 | from itertools import izip 7 | from collections import OrderedDict 8 | import sqlite3 9 | 10 | class PersonExample(object): 11 | ''' Manage one annotated pedestrian example 12 | - all labels are in 'softmax' notation, i.e. (0..N-1) for N classes, 13 | where 0 is the NA label. 14 | - This class provides the functionality to 'mirror' labels 15 | ''' 16 | def __init__(self, attributes, valid_values, 17 | pid, image_filename, box, labels): 18 | self.attributes = attributes 19 | self.valid_values = valid_values 20 | self.pid = pid 21 | self.image_filename = image_filename 22 | self.box = box 23 | self._labels = OrderedDict(zip(self.attributes, labels)) 24 | 25 | def label(self, attr_idx, mirrored=False): 26 | return self.labels(mirrored=mirrored)[attr_idx] 27 | 28 | def labels(self, mirrored=False): 29 | # mirror the labels for this example 30 | if not mirrored: 31 | return self._labels.values() # _labels is an ordered dict 32 | else: 33 | labels_out = [] 34 | binary_mirror_attributes = { 35 | 'HasBagInHandLeft' : 'HasBagInHandRight', 36 | 'HasBagInHandRight' : 'HasBagInHandLeft', 37 | 'HasBagOnShoulderLeft' : 'HasBagOnShoulderRight', 38 | 'HasBagOnShoulderRight': 'HasBagOnShoulderLeft' } 39 | for attr, value in self._labels.items(): 40 | if attr == 'Orientation12': 41 | raise ValueError('we cannot mirror Ori12 yet') 42 | elif attr == 'Orientation4': 43 | # input in softmax notation: 44 | # 0 na, 1 front, 2 back, 3 left, 4 right 45 | # translation permutation: (0)(1)(2) (3,4) 46 | table = {0:0, 1:1, 2:2, 47 | 3:4, 4:3} 48 | try: 49 | out = table[value] 50 | except KeyError: 51 | msg = 'invalid Orientation label in softmax notation: {}' 52 | raise ValueError(msg.format(value)) 53 | labels_out.append(out) 54 | elif attr == 'Orientation8': 55 | # input in softmax notation: 56 | # 0 na, 1 front, 2 front-right, 3 right 57 | # 4 back-right, 5 back, 6 back-left, 7 left, 8 front-left 58 | # translation permutation: (0)(1)(2,8)(3,7)(4,6)(5) 59 | table = {0:0, 1:1, 60 | 2:8, 8:2, 61 | 3:7, 7:3, 62 | 4:6, 6:4, 63 | 5:5} 64 | try: 65 | out = table[value] 66 | except KeyError: 67 | msg = 'invalid Orientation8 label in softmax notation: {}' 68 | raise ValueError(msg.format(value)) 69 | labels_out.append(out) 70 | elif attr in binary_mirror_attributes.keys(): 71 | labels_out.append(self._labels[binary_mirror_attributes[attr]]) 72 | else: 73 | labels_out.append(value) # no change for mirrored example 74 | return labels_out 75 | 76 | def _one_hot(self, idx, max_n): 77 | z = np.zeros(max_n) 78 | z[idx] = 1 79 | return z 80 | 81 | def label_one_hot(self, attr_idx=None, mirrored=False): 82 | label = self.label(attr_idx=attr_idx, mirrored=mirrored) 83 | return self._one_hot(label, self.valid_values[attr_idx]) 84 | 85 | def labels_one_hot(self, attr_idx=None, mirrored=False): 86 | labels = self.labels(mirrored) 87 | return [ self._one_hot(l, self.valid_values[idx]) for idx,l in enumerate(labels) ] 88 | 89 | class DataPreprocessorPARSE(object): 90 | def __init__(self, pathToDataset, 91 | fnAnnoDB='annotations.sqlite3', 92 | attributes='all', 93 | split='train', 94 | exclude_sitting=True, 95 | verbose=False): 96 | """reads annotation from sqlite3 db at fnAnno, and assumes images to be 97 | at pathToDataset 98 | - pathToDataset path to sequence directories containing the images 99 | - fnAnnoDB sqlite3 annotation database 100 | - attributes list of attributes to return 101 | - split which examples to iterate over (train,val,test) 102 | - exclude_sitting - if True treat Posture as binary (NA, walking, standing) 103 | (there are only very few sitting examples, and we did not use these in our work) 104 | - verbose - show SQL query 105 | """ 106 | self.__dict__.update(locals()) 107 | self.pathToSequences = os.path.join(pathToDataset, 'sequences') 108 | 109 | self.ALL_ATTRIBUTES = ('Orientation', 110 | 'Orientation8', 111 | 'Gender', 112 | 'Posture', 113 | 'HasBagOnShoulderLeft', 'HasBagOnShoulderRight', 114 | 'HasBagInHandLeft', 'HasBagInHandRight', 115 | 'HasTrolley', 116 | 'HasBackpack', 117 | 'isPushing', 118 | 'isTalkingOnPhone') 119 | if not self.exclude_sitting: 120 | self.VALID_VALUES = [5,9,3,4,3,3,3,3,3,3,3,3] # max N per item in ALL_ATTRIBUTES 121 | else: 122 | self.VALID_VALUES = [5,9,3,3,3,3,3,3,3,3,3,3] # max N per item in ALL_ATTRIBUTES 123 | 124 | if isinstance(attributes, str) and attributes.lower() == 'all': 125 | self.attributes = self.ALL_ATTRIBUTES 126 | self.valid_values = self.VALID_VALUES 127 | elif isinstance(attributes, list) and len(attributes) > 0: 128 | self.attributes = attributes 129 | self.valid_values = [self.VALID_VALUES[self.ALL_ATTRIBUTES.index(a)] for a in self.attributes] 130 | else: 131 | raise ValueError('invalid attribute selection') 132 | 133 | self.TRAIN_SEQUENCES = (1, 4, 5) 134 | self.VAL_SEQUENCES = (2, 7, 8) 135 | self.TEST_SEQUENCES = (3, 6) 136 | self.TRAINVAL_SEQUENCES = (1, 4, 5, 2, 7, 8) 137 | self.ALL_SEQUENCES = (1, 2, 3, 4, 5, 6, 7, 8) 138 | 139 | if split == 'train': 140 | self.sequenceIDs = self.TRAIN_SEQUENCES 141 | elif split == 'val': 142 | self.sequenceIDs = self.VAL_SEQUENCES 143 | elif split == 'test': 144 | self.sequenceIDs = self.TEST_SEQUENCES 145 | else: 146 | raise ValueError('invalid split') 147 | 148 | self.examples = self._read_examples_from_db() 149 | 150 | def _read_examples_from_db(self): 151 | try: 152 | self.dbFile = os.path.join(self.pathToDataset, self.fnAnnoDB) 153 | self.db = sqlite3.connect(self.dbFile) 154 | self.dbc = self.db.cursor() 155 | except sqlite3.Error as e: 156 | raise Exception(e) 157 | 158 | query = ''' 159 | SELECT s.directory as directory, 160 | i.filename as filename, 161 | p.pedestrianID as pid, 162 | p.box_min_x as min_x, p.box_min_y as min_y, 163 | p.box_max_x as max_x, p.box_max_y as max_y, 164 | {0} 165 | FROM Pedestrian p 166 | INNER JOIN AttributeSet a ON p.attributeSetID = a.attributeSetID 167 | INNER JOIN Image i ON p.imageID = i.imageID 168 | INNER JOIN Sequence s on s.sequenceID = i.sequenceID 169 | '''.format(', '.join((a+'ID' for a in self.attributes))) 170 | if self.exclude_sitting: 171 | query += ' WHERE a.postureID <> 4 ' # filter out all 'sitting' examples 172 | if self.sequenceIDs: 173 | query += 'AND i.sequenceID IN ' + str(self.sequenceIDs) 174 | else: 175 | if self.sequenceIDs: 176 | query += ' WHERE i.sequenceID IN ' + str(self.sequenceIDs) 177 | if self.verbose: 178 | print(query) 179 | 180 | results = self.dbc.execute(query).fetchall() 181 | examples = [] 182 | for row in results: 183 | # tuples are: (PedestrianID, ... all the attributes) 184 | fullFileName = os.path.join(self.pathToSequences, row[0], row[1]) 185 | box = tuple(row[3:7]) 186 | pid = str(row[2]) 187 | labels = self._translate_db_labels_to_softmax(row[7:]) 188 | p = PersonExample(self.attributes, self.valid_values, 189 | pid, fullFileName.encode('utf-8'), 190 | box, labels) 191 | examples.append(p) 192 | self.db.close() 193 | return examples 194 | 195 | def _translate_db_label_to_softmax(self, attribute, label): 196 | """ 197 | translates a label from the sqlite database to a softmax label. 198 | The softmax range is (0,1,...,N) - where 0 is the N/A label. 199 | (0=NA, 1=POS, 2=NEG) 200 | (0=NA, 1=front, 2=back, 3=left, 4=right) 201 | """ 202 | msg = 'unexpected label - attribute: {} - value: {}' 203 | if not isinstance(label, int): 204 | raise TypeError('label expected to be integer') 205 | if not attribute in self.attributes: 206 | raise ValueError('invalid attribute') 207 | 208 | # translate to range [0,1,..N] 209 | # by convention we handled the male as the 'pos' label 210 | # this can have an influence on the exact value of AP scores 211 | if attribute == 'Posture': 212 | if self.exclude_sitting: # if we handle Posture as binary (the default) 213 | if label == 3: # standing -> pos (less frequent) 214 | out = 1 215 | elif label == 2: #walking -> neg (the more frequent class) 216 | out = 2 217 | elif label == 1: 218 | out = 0 219 | else: 220 | raise ValueError(msg.format(attribute, label)) 221 | else: 222 | out = label - 1 223 | return out 224 | 225 | def _translate_db_labels_to_softmax(self, labels): 226 | """ 227 | applies translation all attributes 228 | - should be useful when we support working only on a subset of 229 | attributes 230 | """ 231 | out_labels = [] 232 | if len(self.attributes) != len(labels): 233 | msg = 'length of labels does not match my attribute count!' 234 | raise ValueError(msg) 235 | 236 | out_labels = [self._translate_db_label_to_softmax(a, l) for (a, l) 237 | in izip(self.attributes, labels)] 238 | return out_labels 239 | 240 | @property 241 | def all_examples(self): 242 | return list(self.examples) # return a copy of our internal list 243 | 244 | def __iter__(self): 245 | """iterate over all annotation examples 246 | """ 247 | for example in self.examples: 248 | yield example 249 | -------------------------------------------------------------------------------- /make_up_predictions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Generates random predictions. 4 | This is only to give an example of the dataformat that is expected by 5 | the script: parse_evaluation.py 6 | ''' 7 | 8 | from __future__ import print_function 9 | import h5py 10 | from argparse import ArgumentParser 11 | import numpy as np 12 | 13 | def main(args): 14 | gt = h5py.File(args.groundtruth, 'r') 15 | attribute_names = gt['labels'].attrs['names'] 16 | valid_values = gt['labels'].attrs['valid_values'] 17 | 18 | N = gt['labels'].shape[0] 19 | 20 | out = h5py.File(args.output, 'w') 21 | for idx, attr in enumerate(attribute_names): 22 | K = valid_values[idx] 23 | predictions = np.random.random((N,K)) 24 | if args.mode == 'retrieval': 25 | # make the first column 0 26 | predictions[:,0] = 0 27 | # normalize rows -- keepdims to allow broadcast 28 | sums = np.sum(predictions, axis=1, keepdims=1) 29 | predictions = predictions / sums 30 | out.create_dataset(attr, data=predictions) 31 | out.close() 32 | 33 | if __name__ == '__main__': 34 | parser = ArgumentParser(description=__doc__) 35 | parser.add_argument('--verbose', '-v', action='store_true') 36 | parser.add_argument('groundtruth', help='HDF5 file with groundtruth labels') 37 | parser.add_argument('output', help='filename output') 38 | parser.add_argument('--mode', choices=('retrieval', 'classification'), 39 | default='classification') 40 | args = parser.parse_args() 41 | main(args) 42 | -------------------------------------------------------------------------------- /pUtils/__init__.py: -------------------------------------------------------------------------------- 1 | #vim: set et ts=4 sw=4 2 | # make the 'public' classes visible directly 3 | 4 | from dump_args import dump_args 5 | from fs import recreate_dir 6 | import git 7 | -------------------------------------------------------------------------------- /pUtils/dump_args.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper routine that dumps a argparse.Namespace object to a file 3 | """ 4 | 5 | import os 6 | import json 7 | 8 | 9 | def dump_args(args, directory='./', filename='parameters'): 10 | """dumps variables in args to a file 'parameters' in directory 11 | This can be used to make a record of the exact way a program/script 12 | was called by passing the ArgumentParser object to this. 13 | """ 14 | if not os.path.isdir(directory): 15 | raise IOError('directory does not exist') 16 | out = os.path.join(directory, filename) 17 | if os.path.isfile(out): 18 | raise IOError('the parameters record file already exists!') 19 | 20 | with open(out, 'w') as outfile: 21 | outfile.write(json.dumps(vars(args), sort_keys=True, indent=4)) 22 | -------------------------------------------------------------------------------- /pUtils/fs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to hold filesystem related utility functions. 3 | """ 4 | 5 | import os, shutil 6 | 7 | from pUtils.user_query import query_yes_no 8 | 9 | def recreate_dir(path, prompt_user=True): 10 | """If the directory 'path' is a directory, delete it and recreate it. 11 | If it is not present, make a new directory. 12 | By default the user will be prompted before the directory is removed 13 | """ 14 | if os.path.exists(path) and not os.path.isdir(path): 15 | raise ValueError("path is present, but not a directory") 16 | 17 | if os.path.isdir(path): 18 | query = "-- {0}\n-- the directory exists. Delete it?".format(path) 19 | if prompt_user and not query_yes_no(query): 20 | raise Exception("directory exists.") 21 | else: 22 | shutil.rmtree(path) 23 | # finally, create the directory 24 | os.makedirs(path) 25 | -------------------------------------------------------------------------------- /pUtils/git.py: -------------------------------------------------------------------------------- 1 | """Git helper functions 2 | """ 3 | 4 | import subprocess, os 5 | 6 | 7 | def current_revision(path, short=False): 8 | """takes a 'path' and returns a string representing the 9 | current HEAD git revision. 10 | Optionally, only print a short revision hash. 11 | """ 12 | gitdir = os.path.join(path, '.git') 13 | 14 | if not os.path.isdir(gitdir): 15 | return None 16 | 17 | if short: 18 | cmd = ['git', '--git-dir', gitdir, '--work-tree', path, 19 | 'rev-parse', '--short', 'HEAD' 20 | ] 21 | else: 22 | cmd = ['git', '--git-dir', gitdir, '--work-tree', path, 23 | 'rev-parse', 'HEAD' 24 | ] 25 | 26 | rev = subprocess.check_output(cmd).strip() 27 | 28 | # little workaround to avoid output to stdin/stderr 29 | # the returncode is either 0 - or the exception is raised 30 | try: 31 | subprocess.check_output(['git', '--git-dir', gitdir, 32 | '--work-tree', path, 33 | 'diff', '--exit-code' 34 | ]) 35 | dirty = False 36 | except subprocess.CalledProcessError as e: 37 | dirty = e.returncode 38 | 39 | if dirty: 40 | print 'WARNING: DIRTY GIT REPOSITORY...' 41 | rev = 'DIRTY_GIT_REPO__' + rev 42 | 43 | return rev 44 | -------------------------------------------------------------------------------- /pUtils/user_query.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import sys 3 | 4 | def query_yes_no(question): 5 | """Ask the user a 'yes/no' question. 6 | Make them answer either yes/no - loop until a valid answer has been given. 7 | 8 | Parameter: 9 | question - string to be displayed as question. 10 | 11 | Returns: 12 | Bool - True or False depending on the answer. 13 | 14 | Note: 15 | Inspired by some code found here: 16 | http://code.activestate.com/recipes/577097/ 17 | """ 18 | valid = {"yes": True, "y": True, 19 | "no": False, "n": False} 20 | prompt = " [y/n] " 21 | 22 | while True: 23 | print(question + prompt, end='') 24 | choice = raw_input().lower() 25 | if choice in valid: 26 | return valid[choice] 27 | else: 28 | print("Please respond with 'yes' or 'no' ") 29 | -------------------------------------------------------------------------------- /parse_evaluation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Frontend for evaluation routines on PARSE-27k dataset. 4 | This can be used to evaluate model predictions against the groundtruth. 5 | 6 | The groundtruth is one of the HDF5 files generated by preprocess_dataset.py. 7 | The predictions should be in an HDF5 file - one HDF5 'dataset' for each attribute. 8 | """ 9 | 10 | from __future__ import print_function 11 | import sys 12 | from argparse import ArgumentParser 13 | import numpy as np 14 | import h5py 15 | from collections import OrderedDict 16 | 17 | import pmetrics 18 | 19 | def read_hdf5_groundtruth(filename): 20 | '''read labels attributes from an HDF5 file 21 | Returns: 22 | tuple of (names, labels matrix) 23 | - names is an array of strings specifying the attribute names 24 | - labels matrix is an N x K matrix where K is len(names) 25 | ''' 26 | h5 = h5py.File(filename, 'r') 27 | out = h5['labels'][:] 28 | names = h5['labels'].attrs['names'] 29 | h5.close() 30 | return names, out 31 | 32 | def evaluate_retrieval(groundtruth, predictions): 33 | ''' 34 | Evaluation for the **retrieval viewpoint** 35 | i.e. the first class (idx 0) is treated as N/A (or ?) class 36 | For binary (?, yes, no)-type attributes this computes the AP 37 | 38 | Parameters: 39 | groundtruth: numpy array-like N (N samples - int label specifying the class) 40 | predictions: numpy array-like N x K 41 | 42 | Returns: 43 | dictionary with several evaluation metrics 44 | ''' 45 | measures = {} 46 | confmat = pmetrics.softmax_to_confmat(groundtruth, predictions) 47 | 48 | # check if there were predictions for the first class 49 | # which is not supposed to happen in retrieval mode 50 | if np.any(confmat[:,0] > 0): 51 | raise ValueError('retrieval mode - but there were predictions for N/A class') 52 | 53 | # this compute the two class accuracy - it does not reflect performance on the 54 | # exmaples with N/A groundtruth label 55 | measures['accuracy'] = pmetrics.accuracy(confmat, ignore_na=True) 56 | 57 | if predictions.shape[1] == 3: 58 | # predictions need to be converted to a continous score before 59 | pred_scores = pmetrics.softmax_prediction_to_binary(predictions) 60 | # convert groundtruth to [-1, 1] -- groundtruth input is [0,1,...N] 61 | gt_bin = pmetrics.labels_to_binary(groundtruth) 62 | 63 | measures['AP'] = pmetrics.average_precision_everingham(gt_bin, pred_scores) 64 | else: 65 | # non-binary case, for example Orientation, Orientation8, etc. 66 | # compute specific measures here... if you want. 67 | pass 68 | return measures 69 | 70 | def evaluate_classification(groundtruth, predictions): 71 | ''' 72 | Evaluation for the **classification mode** 73 | i.e. all classes in the groundtruth are 'counted' 74 | 75 | Parameters: 76 | groundtruth: numpy array-like N (N samples - int label specifying the class) 77 | predictions: numpy array-like N x K 78 | 79 | Returns: 80 | dictionary with several evaluation metrics 81 | ''' 82 | measures = {} 83 | confmat = pmetrics.softmax_to_confmat(groundtruth, predictions) 84 | measures['accuracy'] = pmetrics.accuracy(confmat) 85 | measures['BER'] = pmetrics.balanced_error_rate(confmat) 86 | return measures 87 | # 88 | # ----------------------------------------------------------------------------- 89 | # 90 | def evaluate_summary_measures(eval_results, result_field='Summary'): 91 | ''' 92 | Compute averages and the like over results for several attributes. 93 | Here, this just computes the mAP and mBER 94 | 95 | Parameters: 96 | eval_results: dict - attribute -> measures 97 | 98 | Returns: 99 | the eval_results with added result_field (default: 'Summary') 100 | ''' 101 | aps = [] 102 | bers = [] 103 | for attr, measures in eval_results.items(): 104 | if 'AP' in measures.keys(): 105 | aps.append(measures['AP']) 106 | if 'BER' in measures.keys(): 107 | bers.append(measures['BER']) 108 | if len(aps) > 0: 109 | eval_results[result_field] = dict(mAP=np.mean(aps)) 110 | if len(bers) > 0: 111 | eval_results[result_field] = dict(mBER=np.mean(bers)) 112 | return eval_results 113 | 114 | def generate_report(eval_results): 115 | ''' 116 | Parameters: 117 | eval_results: takes a dict - attribute -> measures 118 | where measures is a dict - eval_measures -> values 119 | Returns: 120 | a string representation of the results 121 | ''' 122 | out = 80*'='+'\n\t EVALUATION REPORT \n\n' 123 | for attrib, results in eval_results.items(): 124 | out += 80*'-' + '\n == ' + attrib + ' == \n' 125 | for name, measure in results.items(): 126 | out += '%15s: %s\n' % (str(name), str(measure)) 127 | out += '\n' 128 | return out 129 | 130 | # ------------------------------------------------------------------------------ 131 | # below this point are the routines for using this module as a standalone tool 132 | 133 | def main(args): 134 | ''' this is run when used directly from the commandline 135 | ''' 136 | h5_predictions = h5py.File(args.predictions, 'r') 137 | attributes = h5_predictions.keys() 138 | names, labels = read_hdf5_groundtruth(args.groundtruth) 139 | groundtruth = {} 140 | for idx, name in enumerate(names): 141 | groundtruth[name] = labels[:, idx] 142 | 143 | eval_results = OrderedDict() 144 | report = '' 145 | for attr in attributes: 146 | if args.verbose: 147 | print('evaluating attribute: ', attr) 148 | pred = h5_predictions[attr] 149 | try: 150 | gt = groundtruth[attr] 151 | except KeyError: 152 | print('no groundtruth for attribute: ', attr) 153 | sys.exit(2) 154 | 155 | if args.mode == 'retrieval': 156 | eval_results[attr] = evaluate_retrieval(gt, pred) 157 | elif args.mode == 'classification': 158 | eval_results[attr] = evaluate_classification(gt, pred) 159 | else: 160 | raise NotImplemented('evaluation mode not implemented.') 161 | 162 | eval_results = evaluate_summary_measures(eval_results) 163 | 164 | report = generate_report(eval_results) 165 | # output the results 166 | if args.verbose or args.output is None: 167 | print(report) 168 | if args.output: 169 | f = open(args.output+'.txt', 'w') 170 | f.write(report) 171 | f.close() 172 | 173 | if __name__ == '__main__': 174 | parser = ArgumentParser(description=__doc__) 175 | parser.add_argument('--verbose', '-v', action='store_true') 176 | parser.add_argument('groundtruth', help='HDF5 file with groundtruth labels') 177 | parser.add_argument('predictions', help='HDF5 file with model predictions') 178 | parser.add_argument('--output', '-o', help='filename for eval results report file') 179 | parser.add_argument('--mode', '-m', 180 | choices=('retrieval', 'classification'), default='retrieval', 181 | help='evaluate with N/A labels (i.e. BER) ' 182 | '-- or -- retrieval viewpoint (AP)') 183 | args = parser.parse_args() 184 | main(args) 185 | -------------------------------------------------------------------------------- /pmetrics.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import numpy as np 3 | 4 | from itertools import izip 5 | from matplotlib import pyplot as plt 6 | 7 | """ 8 | Provides a number of useful evaluation metrics 9 | 10 | And helper-routines to compute the metrics 11 | """ 12 | 13 | __version__ = '0.2' 14 | 15 | #------------------------------------------------------------------------------ 16 | # metrics 17 | def average_precision_everingham(groundtruth, predictions): 18 | """ 19 | AP score as defined by Everingham et al. for the PASCAL VOC 20 | Reference: 21 | "The 2005 PASCAL Visual Object Classes Challenge" 22 | Lecture Notes in Computer Science Vol. 3944 23 | Springer 2005 24 | 25 | Parameters: 26 | groundtruth: iterable of labels (valid values: -1,0,1) 27 | 0 labels will be ignored in the computation. 28 | predictions: iterable of predictions (valid values: (-inf, inf) ) 29 | The predictions need to be converted to a continous score before, 30 | i.e. the commonly used softmax output cannot be used directly. 31 | 32 | Return: 33 | AP score 34 | """ 35 | pr, rec = precision_recall(groundtruth, predictions) 36 | 37 | recall_values = np.linspace(0,1,11) # see eq. in reference 38 | eleven_precisions = [] 39 | for r_thresh in recall_values: 40 | # find the max precision value of all threholds where recall is larger than r 41 | pr_values = [ p for p,r in zip(pr,rec) if r >= r_thresh ] 42 | if len(pr_values) == 0: 43 | eleven_precisions.append(0.) 44 | else: 45 | eleven_precisions.append(np.max(pr_values)) 46 | 47 | return np.mean(eleven_precisions) 48 | 49 | def precision_recall(groundtruth, predictions): 50 | """ 51 | Computes precision-recall 52 | For convenience, it handles the special case of possible 53 | 0-labels in the groundtruth, by ignoring predictions for such examples. 54 | 55 | Note: 56 | This seems very lengthy and slowish. 57 | However, in this formulation it is easy to read (and check) and 58 | easily allows to handle special cases like our NA labels in the groundtruth. 59 | 60 | Parameters: 61 | groundtruth: iterable of labels (valid values: -1,0,1) 62 | 0 labels will be ignored in the computation. 63 | predictions: iterable of predictions (valid values: (-inf, inf) ) 64 | The predictions need to be converted to a continous score before, 65 | i.e. the commonly used softmax output cannot be used directly. 66 | 67 | Return: 68 | tuple of (precision, recall) values 69 | """ 70 | # filter NA groundtruth and sort by prediction scores 71 | filtered_predictions = [(p, g) for p, g in izip(predictions, groundtruth) if g != 0] 72 | sorted_predictions = sorted(filtered_predictions, key=lambda x: x[0]) 73 | 74 | tp = len([x for x,g in sorted_predictions if g > 0]) # all positives 75 | fp = len([x for x,g in sorted_predictions if g < 0]) # all negatives 76 | # initially all examples are classified as positives 77 | # then we sweep through the data and adapt tp & fp accordingly 78 | tn = 0 79 | fn = 0 80 | precision = np.zeros(len(sorted_predictions)+1) 81 | recall = np.zeros(len(sorted_predictions)+1) 82 | # handle the special case of the first threshold value 83 | # where everything is classified positive -> perfect recall 84 | precision[0] = tp / float(fp + tp) 85 | recall[0] = tp / float(tp + fn) 86 | for idx, pg in enumerate(sorted_predictions): 87 | _, g = pg 88 | if g < 0: 89 | tn += 1 90 | fp -= 1 91 | elif g > 0: 92 | tp -= 1 93 | fn += 1 94 | 95 | if tp <= 0: 96 | precision[idx+1] = 0. 97 | recall[idx+1] = 0. 98 | else: 99 | precision[idx+1] = tp / float(fp + tp) 100 | recall[idx+1] = tp / float(tp + fn) 101 | return precision, recall 102 | 103 | def accuracy(confmat, ignore_na=False): 104 | """ 105 | Parameters: confusion matrix (columns - predictions - rows groundtruth) 106 | Return: Accuracy percentage (0-100%) 107 | 108 | ignore_na -- if True (default False!) -- this will ignore the first class 109 | which by convention is the NA class 110 | """ 111 | if confmat.shape[0] != confmat.shape[1]: 112 | raise ValueError('non-square confusion matrix') 113 | if len(confmat.shape) > 2: 114 | raise ValueError('more than 2 dimensions in confusion matrix') 115 | N = confmat.shape[0] 116 | correct = 0 117 | errors = 0 118 | for pr_idx in range(N): 119 | for gt_idx in range(N): 120 | if ignore_na: 121 | if pr_idx == 0 or gt_idx == 0: 122 | continue 123 | if pr_idx == gt_idx: 124 | correct += confmat[pr_idx, gt_idx] 125 | else: 126 | errors += confmat[pr_idx, gt_idx] 127 | accuracy = 100. * float(correct) / (correct + errors) 128 | return accuracy 129 | 130 | def balanced_error_rate(confmat): 131 | """ 132 | Compute balanced error rate 133 | 134 | Parameters: confmat as computed by compute_confmat. 135 | Returns: float BER 136 | """ 137 | if confmat.shape[0] != confmat.shape[1]: 138 | raise ValueError('non-square confusion matrix') 139 | if len(confmat.shape) > 2: 140 | raise ValueError('more than 2 dimensions in confusion matrix') 141 | N = confmat.shape[0] 142 | per_class_err = np.zeros(N) 143 | for idx in range(N): 144 | per_class_err[idx] = (np.sum(confmat[idx,:]) - confmat[idx,idx]) / np.sum(confmat[idx,:]) 145 | return np.mean(per_class_err) 146 | 147 | #------------------------------------------------------------------------------ 148 | # helper routines 149 | def softmax_to_confmat(gtlabels, predictions): 150 | """ 151 | gtlabel - index of groundtruth 152 | predictions - label 153 | 154 | returns confusion matrix 155 | """ 156 | N = np.max(gtlabels) 157 | confmat = np.zeros((N+1,N+1)) 158 | for gt, pred in zip(gtlabels, predictions): 159 | pred_idx = np.argmax(pred) 160 | confmat[gt][pred_idx] += 1 161 | return confmat 162 | 163 | def compute_confmat(gtlabels, predictions): 164 | """ compute a confusion matrix 165 | Parameters: 166 | - gtlabels -- the groundtruth (as index in range [0..N) for N classes 167 | - predictions -- same format as groundtruth 168 | Returns: 169 | confusion matrix 170 | """ 171 | N = np.max(gtlabels) 172 | NP = np.max(predictions) 173 | if NP > N: 174 | print('WARNING: predictions and groundtruth do not seem to match') 175 | N = NP 176 | confmat = np.zeros((N+1,N+1)) 177 | for gt, pred in zip(gtlabels, predictions): 178 | confmat[gt][pred] += 1 179 | return confmat 180 | 181 | def softmax_prediction_to_binary(predictions, ignore_na=True): 182 | """ 183 | Transform a softmax prediction for a binary (2+1) attribute, to 184 | a single value, while ignoring the NA class (first column) 185 | This transformation obviously only makes sense for (2 or 2+1 columns). 186 | 187 | Parameters: 188 | predictions np.array N x K entries (K=3) 189 | this assumes that K=0 is the NA label 190 | 191 | Return: 192 | output N x 1 array with class scores 193 | """ 194 | if predictions.shape[1] > 3: 195 | raise ValueError('can only convert binary attributes (binary + N/A)!') 196 | if not ignore_na and np.any( predictions[:,0] > 0 ): 197 | raise ValueError('predictions for NA class - but ignore_na=False') 198 | return predictions[:,1] 199 | 200 | def labels_to_binary(labels): 201 | """ 202 | computes translation: 203 | (0 == 0, 1 == 1, 2 == -1) 204 | Take 205 | labels N x 3 entries 206 | Return 207 | labels N entries (-1,0,1) 208 | """ 209 | if np.any( labels > 2 ): 210 | raise ValueError('too large gt value observed') 211 | if np.any( labels < 0): 212 | raise ValueError('label already has a negative value') 213 | out = np.copy(labels) 214 | out[ labels == 2 ] = -1 215 | return out 216 | -------------------------------------------------------------------------------- /preprocess_dataset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Reads the images, crops pedestrian examples with sufficient additional padding (-p), 5 | and saves the result in a single file for fast loading. 6 | 7 | For convenience, we save two copies of each crop - original and mirrored. 8 | For the mirrored example, the labels are adjusted accordingly. Note, that this is non-trivial 9 | because the bag attributes depend on the orientation. 10 | """ 11 | 12 | from __future__ import print_function 13 | import os, sys, time, os.path as path 14 | from argparse import ArgumentParser 15 | from progressbar import ProgressBar 16 | import concurrent.futures 17 | import h5py 18 | 19 | import numpy as np 20 | from skimage.util import pad 21 | from skimage import transform 22 | from PIL import Image 23 | 24 | from attribute_datasets import DataPreprocessorPARSE as PARSE 25 | from pUtils import git, dump_args, recreate_dir 26 | 27 | def crop(imgfn, box, target_size, padding, padding_mode='zero', mirror=False): 28 | ''' 29 | Load an image and crop bounding box from it. 30 | Handles additional padding - if the box is too close to the image boundary, 31 | the image is padded according to args.padding_mode (i.e. edge or zero) 32 | 33 | Parameters 34 | ---------- 35 | box: the bounding box to be cropped - tuple (min_x, min_y, max_x, max_y) 36 | 37 | target_size: (2-tuple), scale each image s.t. the bounding box 38 | is of height target_size[1] 39 | ( then adapt in x-direction s.t. it matches target_size[0]) 40 | 41 | padding: number of pixels of additional padding around the bounding box 42 | 43 | padding_mode: 'zero' or 'edge' - controls how the padded pixels are filled 44 | 45 | mirror: if true - the resulting crop is mirrored (reversed x-axis) 46 | 47 | Return 48 | ------ 49 | a crop of shape: target_size + padding 50 | type: np.array in ordering [c,w,h] - of type uint8 for compact memory footprint 51 | ''' 52 | img = np.array(Image.open(imgfn).convert('RGB')) 53 | cur_h, cur_w, cur_c = img.shape 54 | box = np.array(box, dtype=np.float64) 55 | 56 | # 1. rescale the whole image s.t. bounding box has target height 57 | # 2. adapt box accordingly (scale to the new image dim, then adapt width) 58 | # 3a. add additional 'padding' to bounding box 59 | # 3b. add padding around the image - in case it is required 60 | # 4. take the crop 61 | # 5. transpose the dimensions 62 | sf = float(target_size[1]) / (box[3]-box[1]) 63 | 64 | # 1. 65 | img_l = transform.resize(img, (np.floor(cur_h*sf+.5), np.floor(cur_w*sf+.5), cur_c)) 66 | sbox = box * sf 67 | 68 | # 2. 69 | delta = (target_size[0]-(sbox[2]-sbox[0])) / 2.0 70 | sbox[0] -= delta 71 | sbox[2] += delta 72 | 73 | sbox = np.floor(sbox +.5).astype(np.int32) 74 | if sbox[2]-sbox[0] <> target_size[0]: 75 | if sbox[2]-sbox[0] - 1 == target_size[0]: 76 | sbox[2] -= 1 77 | else: 78 | raise Exception('new box width does not match target') 79 | if sbox[3]-sbox[1] <> target_size[1]: 80 | if sbox[3]-sbox[1] - 1 == target_size[1]: 81 | sbox[3] -= 1 82 | else: 83 | raise Exception('new box height does not match target') 84 | 85 | # 3a 86 | sbox[0] -= padding # the padding around bounding box (not the whole image) 87 | sbox[1] -= padding 88 | sbox[2] += padding 89 | sbox[3] += padding 90 | 91 | # 3b 92 | if sbox[0] < 0 or sbox[1] < 0 or sbox[2] >= img_l.shape[1] or sbox[3] >= img_l.shape[0]: 93 | pad_offset = np.max(target_size) + padding 94 | sbox = sbox + pad_offset 95 | 96 | if padding_mode == 'edge': 97 | img = pad(img_l, [(pad_offset, pad_offset), 98 | (pad_offset, pad_offset), 99 | (0,0)], mode='edge') 100 | elif padding_mode == 'zero': 101 | img = pad(img_l, [(pad_offset, pad_offset), 102 | (pad_offset, pad_offset), 103 | (0,0)], mode='constant', constant_values=0) 104 | else: 105 | raise NotImplemented('padding mode not implemented: ', padding_mode) 106 | else: 107 | img = img_l # no extra padding around the image required 108 | 109 | # 4. 110 | if mirror: 111 | acrop = img[sbox[1]:sbox[3], sbox[2]:sbox[0]:-1, :] # reversed x-dimension 112 | else: 113 | acrop = img[sbox[1]:sbox[3], sbox[0]:sbox[2], :] 114 | out = (255. * acrop).astype(np.uint8) 115 | return out.transpose((2,1,0)) # transpose to (c,w,h) 116 | 117 | def do_one_crop(e): 118 | c = crop(e.image_filename, e.box, 119 | (args.width, args.height), 120 | padding=args.padding, padding_mode=args.padding_mode, mirror=False) 121 | return c, e.labels(), e.labels(mirrored=True), e.pid 122 | 123 | def preprocess_examples(args, examples, include_mirrors=True): 124 | ''' 125 | ''' 126 | final_width = args.width + 2 * args.padding 127 | final_height = args.height + 2 * args.padding 128 | 129 | N = len(examples) 130 | NN = 2 * N if include_mirrors else N 131 | crops = np.zeros((NN, 3, final_width, final_height), dtype=np.uint8) 132 | labels = [] 133 | labels_mirror = [] # note that the labels do differ for some attributes! 134 | pids = [] 135 | 136 | if not args.single_threaded: 137 | with ProgressBar(max_value=len(examples)) as progress: 138 | pex = concurrent.futures.ProcessPoolExecutor(max_workers=None) 139 | for idx, results in enumerate(pex.map(do_one_crop, examples)): 140 | c, l, lm, p = results # crop, label, labels mirrored 141 | crops[idx, :] = c 142 | labels.append(l) 143 | pids.append(p) 144 | if include_mirrors: 145 | crops[idx+N, :] = c[:, ::-1, :] 146 | labels_mirror.append(lm) 147 | if idx % 50 == 0: 148 | progress.update(idx) 149 | else: 150 | with ProgressBar(max_value=len(examples)) as progress: 151 | for idx, e in enumerate(examples): 152 | if idx % 50 == 0: 153 | progress.update(idx) 154 | c, l, lm, p = do_one_crop(e) 155 | crops[idx, :] = c 156 | labels.append(l) 157 | pids.append(p) 158 | if include_mirrors: 159 | crops[idx+N, :] = c[:, ::-1, :] 160 | labels_mirror.append(lm) 161 | print('') 162 | if include_mirrors: 163 | labels = labels + labels_mirror 164 | pids = pids + pids 165 | return crops, labels, pids 166 | 167 | def write_results(args, split, crops, labels, valid_labels, attribute_names, mean, pids): 168 | if args.output_mode == 'npy': 169 | out = path.join(args.output_dir, split) 170 | os.makedirs(out) 171 | np.save(path.join(out, 'crops'), crops) 172 | np.save(path.join(out, 'labels'), labels) 173 | np.save(path.join(out, 'valid_labels'), valid_labels) 174 | if mean is not None: 175 | np.save(path.join(out, 'mean'), mean) 176 | print('results written as NPY files to directory: ', out) 177 | elif args.output_mode == 'hdf5': 178 | if not path.isdir(args.output_dir): 179 | os.makedirs(args.output_dir) 180 | out = path.join(args.output_dir, split + '.hdf5') 181 | h = h5py.File(out, 'w') 182 | h.create_dataset('crops', data=crops) 183 | # also save the pedestrianID from the database 184 | # this is helpful for debugging and cross-checking with other tools 185 | h.create_dataset('pids', data=pids) 186 | 187 | h.create_dataset('labels', data=labels) 188 | h['labels'].attrs.create('valid_values', valid_labels) 189 | h['labels'].attrs.create('names', attribute_names) 190 | if mean is not None: 191 | h.create_dataset('mean', data=mean) 192 | h.close() # write to file 193 | print('results written as HDF5 to: ', out) 194 | else: 195 | raise NotImplemented('output mode unknown' + args.output_mode) 196 | 197 | def main(args): 198 | for split in ('train', 'val', 'test'): 199 | print('-'*50, '\n', 'processing split: ', split) 200 | 201 | annotations = PARSE(args.parse_path, attributes='all', split=split) 202 | if args.debug: 203 | examples = annotations.all_examples[:500] 204 | else: 205 | examples = annotations.all_examples 206 | 207 | include_mirrors = True if split =='train' and not args.no_train_mirrors else False 208 | crops, labels, pids = preprocess_examples(args, examples, include_mirrors=include_mirrors) 209 | print('extracted %d crops'%len(crops)) 210 | mean = np.mean(crops[:len(examples), :, :, :], axis=0) if split == 'train' else None 211 | valid_labels = examples[0].valid_values 212 | 213 | write_results(args, split, crops, labels, valid_labels, 214 | annotations.attributes, mean, pids) 215 | 216 | 217 | if __name__ == '__main__': 218 | parser = ArgumentParser(description=__doc__) 219 | parser.add_argument('--verbose', '-v', action='store_true') 220 | parser.add_argument('--single_threaded', '-s', action='store_true') 221 | parser.add_argument('--debug', '-d', action='store_true') 222 | parser.add_argument('--output_dir', '-o', 223 | default=os.environ.get('OUTPUT_DIR', '/tmp/parse_crops')) 224 | parser.add_argument('--height', type=int, default=128) 225 | parser.add_argument('--width', type=int, default=64) 226 | parser.add_argument('--padding', '-p', type=int, default=32, 227 | help='# additional padding after scaling to w x h') 228 | parser.add_argument('--output_mode', 229 | choices=('npy', 'hdf5'), 230 | default='hdf5', 231 | help="data format of the resulting files") 232 | parser.add_argument('--padding_mode', '-m', 233 | choices=('zero', 'edge'), 234 | default='edge', 235 | help='''how to pad the crops, in case the 236 | bounding box is too close to the image boundaries.''') 237 | parser.add_argument('--no-train-mirrors', action='store_true', 238 | help="do not generate mirrored examples from" 239 | "train split (by default we do)") 240 | 241 | parser.add_argument('--parse_path', 242 | default=os.getenv('PARSE_PATH', 243 | '/work/sudowe/datasets/parse/')) 244 | args = parser.parse_args() 245 | args.git_revision = git.current_revision(path.dirname(__file__)) 246 | 247 | recreate_dir(args.output_dir) 248 | # save parameters to file (for reproducibility of results) 249 | dump_args(args, args.output_dir) 250 | main(args) 251 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | futures==3.0.3 2 | h5py == 2.5.0 3 | matplotlib == 1.4.3 4 | numpy == 1.10.1 5 | progressbar2 == 3.3.0 6 | scikit_image >= 0.11.3 7 | Pillow == 3.0.0 8 | -------------------------------------------------------------------------------- /visualize_crops.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | 5 | import numpy as np 6 | import h5py 7 | from matplotlib import pyplot as plt 8 | from argparse import ArgumentParser 9 | 10 | def read_crops(args): 11 | if args.verbose: 12 | print('reading from file: ', args.crop_file) 13 | if args.mode == 'npy': 14 | if args.verbose: 15 | print('mode: npy') 16 | data = np.load(args.crop_file) 17 | crops = data['crops'] 18 | return crops, None 19 | elif args.mode == 'hdf5': 20 | if args.verbose: 21 | print('mode: hdf5') 22 | h = h5py.File(args.crop_file, 'r') 23 | # if present this will return the pids - otherwise None 24 | if 'pids' in h.keys(): 25 | pids = h['pids'][:] 26 | else: 27 | pids = None 28 | return h['crops'], pids 29 | else: 30 | raise NotImplemented('mode unknown' + args.output_mode) 31 | 32 | def main(args): 33 | print('loading crops from: ', args.crop_file) 34 | crops, pids = read_crops(args) 35 | print('crops: ', crops.shape) 36 | if pids is not None: 37 | print('read pids...') 38 | 39 | indices = range(len(crops)) 40 | if args.index: 41 | if args.index < 0 or args.index >= len(crops): 42 | print('invalid index - valid are 0..', len(crops)) 43 | sys.exit(1) 44 | indices = [args.index] 45 | if args.random: 46 | np.random.shuffle(indices) 47 | for iii in indices: 48 | print('example idx: ', iii) 49 | c = crops[iii,:] 50 | t = c.transpose((2,1,0)) 51 | o = args.sub 52 | if o > 0: 53 | plt.imshow(t[o:-o,o:-o,:]) 54 | else: 55 | plt.imshow(t) 56 | 57 | title = 'example idx: ' + str(iii) 58 | if pids is not None: 59 | title += 'pid: ' + str(pids[iii]) 60 | plt.title(title) 61 | plt.show() 62 | 63 | 64 | if __name__ == '__main__': 65 | parser = ArgumentParser(description=__doc__) 66 | parser.add_argument('--verbose', '-v', action='store_true') 67 | parser.add_argument('--random', '-r', action='store_true', help='random order') 68 | parser.add_argument('--index', '-i', type=int, help='only visualize this example') 69 | parser.add_argument('--sub', type=int, default=0, 70 | help='only show a centered subcrop (e.g. 8,16,32)') 71 | parser.add_argument('--mode', '-m', 72 | choices=('npy', 'hdf5'), 73 | default='hdf5', 74 | help="data format of the prepared datasets") 75 | parser.add_argument('crop_file') 76 | args = parser.parse_args() 77 | 78 | main(args) 79 | -------------------------------------------------------------------------------- /visualize_pid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Shows the full image in which the pedestrian with pedestrianID 4 | is contained. 5 | This is mainly just to get a quick feel for how to use the annotations. 6 | ''' 7 | 8 | from __future__ import print_function 9 | 10 | import os 11 | import sqlite3 12 | from PIL import Image 13 | import numpy as np 14 | from matplotlib import pyplot as plt 15 | from argparse import ArgumentParser 16 | 17 | def main(args): 18 | try: 19 | dbFile = os.path.join(args.parse_path, 'annotations.sqlite3') 20 | db = sqlite3.connect(dbFile) 21 | dbc = db.cursor() 22 | except sqlite3.Error as e: 23 | raise Exception(e) 24 | 25 | query = '''SELECT directory, filename 26 | FROM Pedestrian p 27 | JOIN Image i ON i.imageID = p.imageID 28 | JOIN Sequence s ON s.sequenceID = i.sequenceID 29 | WHERE p.pedestrianID = {} 30 | ''' 31 | result = dbc.execute(query.format(args.pid)).fetchone() 32 | 33 | imgfn = os.path.join(args.parse_path, 'sequences', result[0], result[1]) 34 | print('image file: ', imgfn) 35 | try: 36 | img = np.array(Image.open(imgfn).convert('RGB')) 37 | except IOError as e: 38 | print('could not load image file') 39 | sys.exit(1) 40 | plt.imshow(img) 41 | plt.show() 42 | 43 | if __name__ == '__main__': 44 | parser = ArgumentParser(description=__doc__) 45 | parser.add_argument('--verbose', '-v', action='store_true') 46 | parser.add_argument('--parse_path', 47 | default=os.getenv('PARSE_PATH', 48 | '/work/sudowe/datasets/parse/')) 49 | parser.add_argument('pid', type=int, 50 | help='show image containing the pedestrian with PID') 51 | args = parser.parse_args() 52 | main(args) 53 | --------------------------------------------------------------------------------