#!/usr/bin/python # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== """Process the ImageNet Challenge bounding boxes for TensorFlow model training. This script is called as process_bounding_boxes.py [synsets-file] Where is a directory containing the downloaded and unpacked bounding box data. If [synsets-file] is supplied, then only the bounding boxes whose synstes are contained within this file are returned. Note that the [synsets-file] file contains synset ids, one per line. The script dumps out a CSV text file in which each line contains an entry. n00007846_64193.JPEG,0.0060,0.2620,0.7545,0.9940 The entry can be read as: , , , , The bounding box for contains two points (xmin, ymin) and (xmax, ymax) specifying the lower-left corner and upper-right corner of a bounding box in *relative* coordinates. The user supplies a directory where the XML files reside. The directory structure in the directory is assumed to look like this: /nXXXXXXXX/nXXXXXXXX_YYYY.xml Each XML file contains a bounding box annotation. The script: (1) Parses the XML file and extracts the filename, label and bounding box info. (2) The bounding box is specified in the XML files as integer (xmin, ymin) and (xmax, ymax) *relative* to image size displayed to the human annotator. The size of the image displayed to the human annotator is stored in the XML file as integer (height, width). Note that the displayed size will differ from the actual size of the image downloaded from image-net.org. To make the bounding box annotation useable, we convert bounding box to floating point numbers relative to displayed height and width of the image. Note that each XML file might contain N bounding box annotations. Note that the points are all clamped at a range of [0.0, 1.0] because some human annotations extend outside the range of the supplied image. See details here: http://image-net.org/download-bboxes (3) By default, the script outputs all valid bounding boxes. If a [synsets-file] is supplied, only the subset of bounding boxes associated with those synsets are outputted. Importantly, one can supply a list of synsets in the ImageNet Challenge and output the list of bounding boxes associated with the training images of the ILSVRC. We use these bounding boxes to inform the random distortion of images supplied to the network. If you run this script successfully, you will see the following output to stderr: > Finished processing 544546 XML files. > Skipped 0 XML files not in ImageNet Challenge. > Skipped 0 bounding boxes not in ImageNet Challenge. > Wrote 615299 bounding boxes from 544546 annotated images. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function import glob import os.path import sys import xml.etree.ElementTree as ET class BoundingBox(object): pass def GetItem(name, root, index=0): count = 0 for item in root.iter(name): if count == index: return item.text count += 1 # Failed to find "index" occurrence of item. return -1 def GetInt(name, root, index=0): return int(GetItem(name, root, index)) def FindNumberBoundingBoxes(root): index = 0 while True: if GetInt('xmin', root, index) == -1: break index += 1 return index def ProcessXMLAnnotation(xml_file): """Process a single XML file containing a bounding box.""" # pylint: disable=broad-except try: tree = ET.parse(xml_file) except Exception: print('Failed to parse: ' + xml_file, file=sys.stderr) return None # pylint: enable=broad-except root = tree.getroot() num_boxes = FindNumberBoundingBoxes(root) boxes = [] for index in xrange(num_boxes): box = BoundingBox() # Grab the 'index' annotation. box.xmin = GetInt('xmin', root, index) box.ymin = GetInt('ymin', root, index) box.xmax = GetInt('xmax', root, index) box.ymax = GetInt('ymax', root, index) box.width = GetInt('width', root) box.height = GetInt('height', root) box.filename = GetItem('filename', root) + '.JPEG' box.label = GetItem('name', root) xmin = float(box.xmin) / float(box.width) xmax = float(box.xmax) / float(box.width) ymin = float(box.ymin) / float(box.height) ymax = float(box.ymax) / float(box.height) # Some images contain bounding box annotations that # extend outside of the supplied image. See, e.g. # n03127925/n03127925_147.xml # Additionally, for some bounding boxes, the min > max # or the box is entirely outside of the image. min_x = min(xmin, xmax) max_x = max(xmin, xmax) box.xmin_scaled = min(max(min_x, 0.0), 1.0) box.xmax_scaled = min(max(max_x, 0.0), 1.0) min_y = min(ymin, ymax) max_y = max(ymin, ymax) box.ymin_scaled = min(max(min_y, 0.0), 1.0) box.ymax_scaled = min(max(max_y, 0.0), 1.0) boxes.append(box) return boxes if __name__ == '__main__': if len(sys.argv) < 2 or len(sys.argv) > 3: print('Invalid usage\n' 'usage: process_bounding_boxes.py [synsets-file]', file=sys.stderr) sys.exit(-1) xml_files = glob.glob(sys.argv[1] + '/*/*.xml') print('Identified %d XML files in %s' % (len(xml_files), sys.argv[1]), file=sys.stderr) if len(sys.argv) == 3: labels = set([l.strip() for l in open(sys.argv[2]).readlines()]) print('Identified %d synset IDs in %s' % (len(labels), sys.argv[2]), file=sys.stderr) else: labels = None skipped_boxes = 0 skipped_files = 0 saved_boxes = 0 saved_files = 0 for file_index, one_file in enumerate(xml_files): # Example: <...>/n06470073/n00141669_6790.xml label = os.path.basename(os.path.dirname(one_file)) # Determine if the annotation is from an ImageNet Challenge label. if labels is not None and label not in labels: skipped_files += 1 continue bboxes = ProcessXMLAnnotation(one_file) assert bboxes is not None, 'No bounding boxes found in ' + one_file found_box = False for bbox in bboxes: if labels is not None: if bbox.label != label: # Note: There is a slight bug in the bounding box annotation data. # Many of the dog labels have the human label 'Scottish_deerhound' # instead of the synset ID 'n02092002' in the bbox.label field. As a # simple hack to overcome this issue, we only exclude bbox labels # *which are synset ID's* that do not match original synset label for # the XML file. if bbox.label in labels: skipped_boxes += 1 continue # Guard against improperly specified boxes. if (bbox.xmin_scaled >= bbox.xmax_scaled or bbox.ymin_scaled >= bbox.ymax_scaled): skipped_boxes += 1 continue # Note bbox.filename occasionally contains '%s' in the name. This is # data set noise that is fixed by just using the basename of the XML file. image_filename = os.path.splitext(os.path.basename(one_file))[0] print('%s.JPEG,%.4f,%.4f,%.4f,%.4f' % (image_filename, bbox.xmin_scaled, bbox.ymin_scaled, bbox.xmax_scaled, bbox.ymax_scaled)) saved_boxes += 1 found_box = True if found_box: saved_files += 1 else: skipped_files += 1 if not file_index % 5000: print('--> processed %d of %d XML files.' % (file_index + 1, len(xml_files)), file=sys.stderr) print('--> skipped %d boxes and %d XML files.' % (skipped_boxes, skipped_files), file=sys.stderr) print('Finished processing %d XML files.' % len(xml_files), file=sys.stderr) print('Skipped %d XML files not in ImageNet Challenge.' % skipped_files, file=sys.stderr) print('Skipped %d bounding boxes not in ImageNet Challenge.' % skipped_boxes, file=sys.stderr) print('Wrote %d bounding boxes from %d annotated images.' % (saved_boxes, saved_files), file=sys.stderr) print('Finished.', file=sys.stderr)