From 5733d46f37fb591fcb7c5da09e7dda8742f7a765 Mon Sep 17 00:00:00 2001 From: Joaquin Alori Date: Wed, 14 Feb 2018 13:07:02 -0300 Subject: [PATCH 1/2] Add possibility to merge several classes to dataset scripts --- .../dataset/readers/object_detection/coco.py | 16 +++++++++++++--- .../object_detection/object_detection_reader.py | 4 +++- .../readers/object_detection/pascalvoc.py | 13 +++++++++---- luminoth/tools/dataset/transform.py | 17 ++++++++++------- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/luminoth/tools/dataset/readers/object_detection/coco.py b/luminoth/tools/dataset/readers/object_detection/coco.py index cb6d801b..834330f7 100644 --- a/luminoth/tools/dataset/readers/object_detection/coco.py +++ b/luminoth/tools/dataset/readers/object_detection/coco.py @@ -45,14 +45,24 @@ def __init__(self, data_dir, split, year=DEFAULT_YEAR, for annotation in annotations_json['annotations']: image_id = annotation['image_id'] x, y, width, height = annotation['bbox'] + if not self._merge_classes: + try: + label_id = self.classes.index( + category_to_name[annotation['category_id']] + ) + except ValueError: + # Class may have gotten filtered by: + # --only-classes or --limit-classes + continue + else: + label_id = 0 + self._image_to_bboxes.setdefault(image_id, []).append({ 'xmin': x, 'ymin': y, 'xmax': x + width, 'ymax': y + height, - 'label': self.classes.index( - category_to_name[annotation['category_id']] - ), + 'label': label_id, }) self._image_to_details = {} diff --git a/luminoth/tools/dataset/readers/object_detection/object_detection_reader.py b/luminoth/tools/dataset/readers/object_detection/object_detection_reader.py index 9f58042e..286000ab 100644 --- a/luminoth/tools/dataset/readers/object_detection/object_detection_reader.py +++ b/luminoth/tools/dataset/readers/object_detection/object_detection_reader.py @@ -23,7 +23,8 @@ class ObjectDetectionReader(BaseReader): Iterate over all records. """ def __init__(self, only_classes=None, only_images=None, - limit_examples=None, limit_classes=None, seed=None, **kwargs): + limit_examples=None, limit_classes=None, merge_classes=False, + seed=None, **kwargs): """ Args: - only_classes: string or list of strings used as a class @@ -47,6 +48,7 @@ def __init__(self, only_classes=None, only_images=None, self._limit_examples = limit_examples self._limit_classes = limit_classes + self._merge_classes = merge_classes random.seed(seed) self._total = None diff --git a/luminoth/tools/dataset/readers/object_detection/pascalvoc.py b/luminoth/tools/dataset/readers/object_detection/pascalvoc.py index 0d6bf32e..b3b1cec9 100644 --- a/luminoth/tools/dataset/readers/object_detection/pascalvoc.py +++ b/luminoth/tools/dataset/readers/object_detection/pascalvoc.py @@ -97,10 +97,15 @@ def iterate(self): gt_boxes = [] for b in annotation['object']: - try: - label_id = self.classes.index(b['name']) - except ValueError: - continue + if not self._merge_classes: + try: + label_id = self.classes.index(b['name']) + except ValueError: + # Class may have gotten filtered by: + # --only-classes or --limit-classes + continue + else: + label_id = 0 gt_boxes.append({ 'label': label_id, diff --git a/luminoth/tools/dataset/transform.py b/luminoth/tools/dataset/transform.py index 89ed278d..b139011e 100644 --- a/luminoth/tools/dataset/transform.py +++ b/luminoth/tools/dataset/transform.py @@ -14,7 +14,7 @@ def get_output_subfolder(only_classes, only_images, limit_examples, Returns: subfolder name for records """ if only_classes is not None: - return 'classes-{}'.format(only_classes) + return 'classes-{}'.format('-'.join(only_classes)) elif only_images is not None: return 'only-{}'.format(only_images) elif limit_examples is not None and limit_classes is not None: @@ -30,7 +30,8 @@ def get_output_subfolder(only_classes, only_images, limit_examples, @click.option('--data-dir', help='Where to locate the original data.') @click.option('--output-dir', help='Where to save the transformed data.') @click.option('splits', '--split', required=True, multiple=True, help='Which splits to transform.') # noqa -@click.option('--only-classes', help='Whitelist of classes.') +@click.option('--only-classes', multiple=True, help='Whitelist of classes.') +@click.option('--merge-classes', help='Merge all classes into a single class') @click.option('--only-images', help='Create dataset with specific examples.') @click.option('--limit-examples', type=int, help='Limit dataset with to the first `N` examples.') # noqa @click.option('--limit-classes', type=int, help='Limit dataset with `N` random classes.') # noqa @@ -38,8 +39,8 @@ def get_output_subfolder(only_classes, only_images, limit_examples, @click.option('overrides', '--override', '-o', multiple=True, help='Custom parameters for readers.') # noqa @click.option('--debug', is_flag=True, help='Set level logging to DEBUG.') def transform(dataset_reader, data_dir, output_dir, splits, only_classes, - only_images, limit_examples, limit_classes, seed, overrides, - debug): + merge_classes, only_images, limit_examples, limit_classes, seed, + overrides, debug): """ Prepares dataset for ingestion. @@ -67,6 +68,8 @@ def transform(dataset_reader, data_dir, output_dir, splits, only_classes, # All splits must have a consistent set of classes. classes = None + merge_classes = merge_classes in ('True', 'true', 'TRUE') + reader_kwargs = parse_override(overrides) try: @@ -74,9 +77,9 @@ def transform(dataset_reader, data_dir, output_dir, splits, only_classes, # Create instance of reader. split_reader = reader( data_dir, split, - only_classes=only_classes, only_images=only_images, - limit_examples=limit_examples, limit_classes=limit_classes, - seed=seed, **reader_kwargs + only_classes=only_classes, merge_classes=merge_classes, + only_images=only_images, limit_examples=limit_examples, + limit_classes=limit_classes, seed=seed, **reader_kwargs ) if classes is None: From 40ac9233b881c736b474d2868ae9508a35e8a96f Mon Sep 17 00:00:00 2001 From: Joaquin Alori Date: Mon, 19 Feb 2018 17:59:56 -0300 Subject: [PATCH 2/2] Make classes.json file created by 'transform' consider merged classes Now when there are merged classes the class.json file, which is used for labeling predicted videos/pics, will be empty. This fixes a problem in which all predictions generated from a dataset that had merged classes would be labeled with the label of the first class. Eg.: If you generated a dataset based on coco that filtered all classes except car, bus, and truck, and that also merged these classes into a single class. The predictions generated by predict.py or the web server would be labeled as car, when in fact they could've been a truck or a bus too. The idea behind completely removing the label instead of picking a new one is that a model that was trained to predict a single type of object, would portray this information more globally, instead of on an per object basis. For example in the name of the .jpg or .mpg file it created, or something like this. Still, there are probably use cases in which it would be useful to let the user pick the label. This could be added in the future after choosing a good console argument name for this parameter, and seeing if we could somehow merge it with the --merge argument while also maintaining the ability to have the label be empty. --- luminoth/tools/dataset/readers/object_detection/coco.py | 2 +- .../readers/object_detection/object_detection_reader.py | 2 +- .../tools/dataset/readers/object_detection/pascalvoc.py | 2 +- luminoth/tools/dataset/writers/object_detection_writer.py | 7 ++++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/luminoth/tools/dataset/readers/object_detection/coco.py b/luminoth/tools/dataset/readers/object_detection/coco.py index 834330f7..3090cb61 100644 --- a/luminoth/tools/dataset/readers/object_detection/coco.py +++ b/luminoth/tools/dataset/readers/object_detection/coco.py @@ -45,7 +45,7 @@ def __init__(self, data_dir, split, year=DEFAULT_YEAR, for annotation in annotations_json['annotations']: image_id = annotation['image_id'] x, y, width, height = annotation['bbox'] - if not self._merge_classes: + if not self.merge_classes: try: label_id = self.classes.index( category_to_name[annotation['category_id']] diff --git a/luminoth/tools/dataset/readers/object_detection/object_detection_reader.py b/luminoth/tools/dataset/readers/object_detection/object_detection_reader.py index 286000ab..7fdadf54 100644 --- a/luminoth/tools/dataset/readers/object_detection/object_detection_reader.py +++ b/luminoth/tools/dataset/readers/object_detection/object_detection_reader.py @@ -48,7 +48,7 @@ def __init__(self, only_classes=None, only_images=None, self._limit_examples = limit_examples self._limit_classes = limit_classes - self._merge_classes = merge_classes + self.merge_classes = merge_classes random.seed(seed) self._total = None diff --git a/luminoth/tools/dataset/readers/object_detection/pascalvoc.py b/luminoth/tools/dataset/readers/object_detection/pascalvoc.py index b3b1cec9..1482589f 100644 --- a/luminoth/tools/dataset/readers/object_detection/pascalvoc.py +++ b/luminoth/tools/dataset/readers/object_detection/pascalvoc.py @@ -97,7 +97,7 @@ def iterate(self): gt_boxes = [] for b in annotation['object']: - if not self._merge_classes: + if not self.merge_classes: try: label_id = self.classes.index(b['name']) except ValueError: diff --git a/luminoth/tools/dataset/writers/object_detection_writer.py b/luminoth/tools/dataset/writers/object_detection_writer.py index e826b446..6dbf2d82 100644 --- a/luminoth/tools/dataset/writers/object_detection_writer.py +++ b/luminoth/tools/dataset/writers/object_detection_writer.py @@ -54,7 +54,12 @@ def save(self): # Save classes in simple json format for later use. classes_file = os.path.join(self._output_dir, CLASSES_FILENAME) - json.dump(self._reader.classes, tf.gfile.GFile(classes_file, 'w')) + if self._reader.merge_classes: + # Don't assign a name to the class if its a merge of several others + json.dump([''], tf.gfile.GFile(classes_file, 'w')) + else: + json.dump(self._reader.classes, tf.gfile.GFile(classes_file, 'w')) + record_file = os.path.join( self._output_dir, '{}.tfrecords'.format(self._split)) writer = tf.python_io.TFRecordWriter(record_file)