Source code for justdeepit.utils

import os
import sys
import re
import gzip
import json
import base64
import hashlib
import random
import string
import glob
import pkg_resources
import xml.etree.ElementTree as ET
import numpy as np
import cv2
import skimage
import skimage.io
import skimage.color
import skimage.measure
import skimage.draw
import PIL.Image
import PIL.ImageOps



class JsonEncoder(json.JSONEncoder):
    """Convert NumPy object to JSON object
    
    Convert NumPy objects to JSON object during writting process.
    
    Examples:
        >>> import json
        >>>
        >>> data_dict = {'bbox': [1, 2, 3, 4], 'image': '/path/to/image.jpg'}
        >>> json.dump(data_dict, cls=JsonEncoder)
    
    """
    
    def default(self, obj):
        if isinstance(obj, np.integer):
            return int(obj)
        elif isinstance(obj, np.floating):
            return float(obj)
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        elif isinstance(obj, np.nan):
            return None
        else:
            return super(JsonEncoder, self).default(obj)



[docs] class ImageAnnotation: """A container to store image annotations Class :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` is a container that stores image annotations such as file paths, coordinates of bounding boxes and object contours, and object names. Args: image_path (str): A path to an image. annotation (str): A path to an annotation file that corresponds to the image ``image_path``. annotation_format (str): A string to specify an annotation format. If ``auto`` is specified, then automatically estimate the annotation format. rgb2class (dict): A dictionary mapping RGB values to class name. The image annotations are stored with the following attributes: Attributes: image_path (str): A path to an image. image (numpy.ndarray): An image data in :py:class:`numpy.ndarray` object, which is generated by :py:func:`skimage.io.imread`. exif_orientation (int): An integer ranged from 1 until 8 to specify EXIF Orientation. regions (list): A list of dictionaries consists of image annotations, and each dictionary consists of keys ``id``, ``class``, ``bbox``, ``contour``, and ``score``. ``id`` is an integer that stores the object identifier, and ``class`` is a string that stores a class label. ``bbox`` is a list consisting of four elements (``[xmin, ymin, xmax, ymax]``) that represent the coordinates of the bounding box, and ``contour`` is a two-dimensional array (e.g., ``[[x1, y1], [x2, y2], ..., [xn, yn]]``) that stores the coordinates of object contours. ``score`` is the confidence score of the class, which is usually output from object detection models. If the object is a donut-shaped object (i.e., object with several holes), the holes are also annotated as an object, but the ``class`` is set to *__background__* in this case. mask (numpy.ndarray): A mask of image stored in :py:class:`numpy.ndarray`. class2rgb (dict): A dictionary mapping class name to RGB values. rgb2class (dict): A dictionary mapping RGB values to class name. Class :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` mainly implements methods :func:`format <justdeepit.utils.ImageAnnotation.format>` and :func:`draw <justdeepit.utils.ImageAnnotation.draw>` for format conversion and image visualization, respectively. By using method :func:`format <justdeepit.utils.ImageAnnotation.format>`, the annotations can be converted to any format, such as the COCO format and Pascal VOC format. By using method :func:`draw <justdeepit.utils.ImageAnnotation.draw>`, the image is plotted with annotations. Examples: >>> from justdeepit.utils import ImageAnnotation >>> >>> image_fpath = './path/to/image.jpg' >>> ann_fpath = './path/to/image.json' >>> >>> ann = ImageAnnotation(image_fpath, ann_fpath) """ def __init__(self, image_path, annotation, annotation_format='auto', class2rgb=None, rgb2class=None): self.image_path = image_path self.image, self.exif_orientation = self.__imread(image_path) self.class2rgb, self.rgb2class = self.__set_colormapping_dict(class2rgb, rgb2class) self.regions = self.__set_regions(annotation, annotation_format) self.mask = None self.__rgb = [(0, 48, 73), (251, 143, 103), (33, 131, 128), (106, 76, 147), (240, 200, 8), (251, 176, 45), (92, 128, 1), (7, 160, 195), (155, 93, 229), (221, 28, 26), (251, 97, 7), (255, 241, 208), (255, 89, 94), (8, 103, 136), (70, 73, 76), (216, 17, 18), (25, 130, 196), (253, 240, 213), (241, 91, 181), (143, 45, 86), (255, 194, 180), (124, 181, 24), (115, 210, 222), (0, 187, 259), (25, 133, 161), (254, 228, 64), (120, 0, 0), (102, 155, 188), (21, 96, 100), (248, 225, 108)] def __imread(self, image_path): image = PIL.Image.open(image_path) exif_o = None if hasattr(image, '_getexif') and image._getexif() is not None: exif_o = image._getexif().get(0x112, 1) return np.array(PIL.ImageOps.exif_transpose(image)), exif_o def __estimate_annotation_format(self, ann): """Estimate annotation foramt If file path is given, then load annotations and estimate the format by checking defined keys If python objects is given, then estimate the format by checking defiend keys """ if isinstance(ann, str): if ann.lower().endswith(('.png', '.jpg', '.jpeg', '.tiff', '.bmp', '.gif')): return 'rgbmask' else: fdat = ann if (os.path.exists(ann)): fdat = '' with open(ann, 'r') as infh: for _ in infh: fdat += _ if '<annotation>' in fdat and '<object>' in fdat and '<source>' in fdat and '<size>' in fdat: return 'voc' elif 'categories' in fdat and 'images' in fdat and 'annotations' in fdat and 'image_id': return 'coco' elif 'asset' in fdat and 'regions' in fdat: return 'vott' elif isinstance(ann, dict): if 'categories' in ann and 'images' in ann and 'annotations' in ann: return 'coco' elif 'asset' in ann and 'regions' in ann: return 'vott' elif isinstance(ann, list): if len(ann) == 0: return None elif isinstance(ann[0], dict) and 'id' in ann[0] and 'class' in ann[0] and 'bbox' in ann[0]: return 'base' elif isinstance(ann, np.ndarray): return 'array' raise ValueError('Failure to parse annotation format.') def __set_regions(self, annotation, annotation_format='auto'): if annotation is None: return [] if annotation_format == 'auto': annotation_format = self.__estimate_annotation_format(annotation) if annotation_format == None: return annotation if annotation_format == 'base': return annotation elif annotation_format == 'json': return self.__set_regions_from_json(annotation) elif annotation_format == 'array': return self.__set_regions_from_array(annotation) elif annotation_format == 'rgbmask': return self.__set_regions_from_rgbmask(annotation) elif annotation_format == 'vott': return self.__set_regions_from_vott(annotation) elif 'voc' in annotation_format: return self.__set_regions_from_voc(annotation) elif annotation_format == 'coco': return self.__set_regions_from_coco(annotation) else: raise ValueError('Undefined format.') def __set_regions_from_json(self, json_fpath): with open(json_fpath, 'r') as infh: regions = json.load(infh) return regions def __set_regions_from_coco(self, coco): regions = [] coco_data = None with open(coco, 'r') as jsonfh: coco_data = json.load(jsonfh) # find image ID image_id = None for coco_image in coco_data['images']: if coco_image['file_name'] == os.path.basename(self.image_path): image_id = str(coco_image['id']) # get class ID and name cateid2name = {} for coco_cate in coco_data['categories']: cateid2name[coco_cate['id']] = coco_cate['name'] # get region annotations regions = [] for coco_region in coco_data['annotations']: if str(coco_region['image_id']) == image_id: region = { 'id': coco_region['id'], 'bbox': [coco_region['bbox'][0], coco_region['bbox'][1], coco_region['bbox'][0] + coco_region['bbox'][2], coco_region['bbox'][1] + coco_region['bbox'][3]], 'class': cateid2name[coco_region['category_id']], 'score': np.nan, } if 'segmentation' in coco_region and len(coco_region['segmentation']) > 0: region['contour'] = np.array(coco_region['segmentation']).reshape(-1, 2, order='C') regions.append(region) return regions def __set_regions_from_array(self, mask): regions = [] mask = mask.copy() if len(mask.shape) > 3: mask = skimage.color.rgb2gray(mask) mask_h, mask_w = mask.shape mask_padding = 10 padded_mask = np.zeros((mask_h + 2 * mask_padding, mask_w + 2 * mask_padding)) padded_mask[mask_padding:(mask_padding + mask_h), mask_padding:(mask_padding + mask_w)] = mask mask_contours = skimage.measure.find_contours(padded_mask, 0.5) for n, contour in enumerate(mask_contours): rr, cc = skimage.draw.polygon(contour[:, 0], contour[:, 1], padded_mask.shape[0:2]) contour = contour.astype(np.int32) - mask_padding contour[contour < 0] = 0 contour[contour[:, 0] > mask_h -1, 0] = mask_h - 1 contour[contour[:, 1] > mask_w-1, 1] = mask_w - 1 regions.append({ 'id' : n + 1, 'class' : '__background__' if np.mean(padded_mask[rr, cc] / (np.max(padded_mask[rr, cc]) + 0.01)) < 0.5 else 'object', 'bbox' : [np.min(contour[:, 1]), np.min(contour[:, 0]), np.max(contour[:, 1]), np.max(contour[:, 0])], 'contour': contour[:, [1, 0]], 'score' : np.nan, }) return regions def __set_regions_from_rgbmask(self, mask): regions = [] mask, _ = self.__imread(mask) mask = mask[:, :, 0:3] # RGB mask mask_h, mask_w = mask.shape[0:2] mask_padding = 10 padded_mask = np.zeros((mask_h + 2 * mask_padding, mask_w + 2 * mask_padding, mask.shape[2])) padded_mask[mask_padding:(mask_padding + mask_h), mask_padding:(mask_padding + mask_w)] = mask # make masks for each objects and save them into a dictionary # the key is object color (RGB) and value is the binary array of objects padded_mask_dict = {} for h in range(padded_mask.shape[0]): for w in range(padded_mask.shape[1]): if (np.sum(padded_mask[h, w, :]) > 0): # skip the black color (no object) px_rgb = ','.join([str(int(_)) for _ in padded_mask[h, w, :]]) if px_rgb not in padded_mask_dict: padded_mask_dict[px_rgb] = np.zeros(padded_mask.shape[0:2]) padded_mask_dict[px_rgb][h, w] = 1 # detect objects from each mask object_id = 0 for object_class_i, padded_mask_i in padded_mask_dict.items(): mask_contours = skimage.measure.find_contours(padded_mask_i, 0.5) for n, contour in enumerate(mask_contours): rr, cc = skimage.draw.polygon(contour[:, 0], contour[:, 1], padded_mask.shape[0:2]) contour = contour.astype(np.int32) - mask_padding contour[contour < 0] = 0 contour[contour[:, 0] > mask_h - 1, 0] = mask_h - 1 contour[contour[:, 1] > mask_w - 1, 1] = mask_w - 1 if self.rgb2class is not None and object_class_i in self.rgb2class: object_class_i = self.rgb2class[object_class_i] regions.append({ 'id' : object_id, 'class' : '__background__' if np.mean(padded_mask_i[rr, cc] / (np.max(padded_mask_i[rr, cc]) + 0.01)) < 0.5 else object_class_i, 'bbox' : [np.min(contour[:, 1]), np.min(contour[:, 0]), np.max(contour[:, 1]), np.max(contour[:, 0])], 'contour': contour[:, [1, 0]], 'score' : np.nan, }) object_id += 1 return regions def __set_regions_from_voc(self, voc): regions = [] is_obj = False obj_id = 0 obj_name = '' obj_bbox = [None, None, None, None] voc_tree = ET.parse(voc) voc_root = voc_tree.getroot() obj_id = 0 for voc_data in voc_root: if voc_data.tag == 'object': regions.append({ 'id': obj_id, 'class': voc_data.find('name').text, 'bbox': [ float(voc_data.find('bndbox').find('xmin').text), float(voc_data.find('bndbox').find('ymin').text), float(voc_data.find('bndbox').find('xmax').text), float(voc_data.find('bndbox').find('ymax').text) ], 'score': np.nan, }) obj_id += 1 return regions def __set_regions_from_vott(self, vott): regions = [] vott_data = None with open(vott, 'r') as jsonfh: vott_data = json.load(jsonfh) # find image and annotaiton from the multi-entry VoTT annotation vott_regions = None for vott_image_id, vott_image_meta in vott_data['assets'].items(): if vott_image_meta['asset']['name'] == os.path.basename(self.image_path): vott_regions = vott_image_meta['regions'] break if vott_regions is not None: for vott_region in vott_regions: bbox_xy = [[int(np.floor(vott_region['boundingBox']['left'])), int(np.floor(vott_region['boundingBox']['top']))], [int(np.floor(vott_region['boundingBox']['left'])) + \ int(np.floor(vott_region['boundingBox']['width'])), int(np.floor(vott_region['boundingBox']['top'])) + \ int(np.floor(vott_region['boundingBox']['height']))]] polygon_xy = [[int(np.floor(p['x'])), int(np.floor(p['y']))] for p in vott_region['points']] bbox_xy = self.__exif_transpose(bbox_xy, self.image.shape[0:2], self.exif_orientation) polygon_xy = self.__exif_transpose(polygon_xy, self.image.shape[0:2], self.exif_orientation) if bbox_xy[0][0] > bbox_xy[1][0]: bbox_xy[0][0], bbox_xy[1][0] = bbox_xy[1][0], bbox_xy[0][0] if bbox_xy[0][1] > bbox_xy[1][1]: bbox_xy[0][1], bbox_xy[1][1] = bbox_xy[1][1], bbox_xy[0][1] for vott_tag in vott_region['tags']: region = { 'id' : vott_region['id'] + '_' + vott_tag, 'class': vott_tag, 'bbox': [bbox_xy[0][0], bbox_xy[0][1], bbox_xy[1][0], bbox_xy[1][1]], 'contour' : np.array(polygon_xy), 'score': np.nan, } regions.append(region) return regions def __exif_transpose(self, points, image_size, exif_orientation): """Transpose coordinates according to EXIF orientation Transpose coordinates accroding to EXIF orientation. Args: points (list): A list containes coordinates. Each element is a list contained two elements which are the coordinates of x and y. image_size (list): Image width and height after EXIF transpose. exif_orientation (int): EXIF orientation ID. Return: A list contains transposed coordinates. """ if exif_orientation is None or exif_orientation == 1: pass elif exif_orientation == 2: for i in range(len(points)): points[i][0] = image_size[0] - (points[i][0] + 1) raise ValueError('Orientation 2 is not validated so far, check it manually.' + self.image_path) elif exif_orientation == 3: for i in range(len(points)): points[i][0] = image_size[1] - points[i][0] points[i][1] = image_size[0] - points[i][1] elif exif_orientation == 4: for i in range(len(points)): points[i][1] = image_size[1] - (points[i][1] + 1) raise ValueError('Orientation 4 is not validated so far, check it manually.' + self.image_path) elif exif_orientation == 5: for i in range(len(points)): points[i][1] = image_size[1] - (points[i][1] + 1) for i in range(len(points)): points[i][0], points[i][1] = points[i][1], image_size[1] - (points[i][0] + 1) raise ValueError('Orientation 5 is not validated so far, check it manually.' + self.image_path) elif exif_orientation == 6: for i in range(len(points)): points[i][0], points[i][1] = image_size[1] - points[i][1], points[i][0] elif exif_orientation == 7: for i in range(len(points)): points[i][1] = image_size[1] - (points[i][1] + 1) for i in range(len(points)): points[i][0], points[i][1] = image_size[0] - (points[i][1] + 1), points[i][0] raise ValueError('Orientation 7 is not validated so far, check it manually.' + self.image_path) elif exif_orientation == 8: for i in range(len(points)): points[i][0], points[i][1] = points[i][1], image_size[0] - points[i][0] else: raise ValueError('Unknown EXIF orientation.') return points
[docs] def format(self, annotation_format, file_path=None): """Format annotation to specific format Method :func:`format <justdeepit.utils.ImageAnnotation.format>` converts class object :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` into a specific format. COCO (:file:`.json`) and Pascal VOC (:file:`.xml`) are supported formats in the current version of JustDeepIt. Additionally, this method supports the conversion of class object :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` into a mask image represented by class object :class:`numpy.ndarray`. Args: annotation_format (str): A string to specify the format to be formatted. ``json``, ``coco``, ``voc``, or ``mask`` can be specified. file_path (str): A path to save the converted annotation. If ``None`` is given, return the converted annotation in string or dictionary. Returns: If ``file_path`` is ``None``, return a string (for Pascal VOC format), a dictionary (for COCO), or :class:`numpy.ndarray` (for image data). Otherwise, save the data in the given path. Examples: >>> from justdeepit.utils import ImageAnnotation >>> >>> image_fpath = './path/to/image.jpg' >>> ann_fpath = './path/to/image.json' >>> ann = ImageAnnotation(image_fpath, ann_fpath) >>> >>> ann.format('voc', './path/to/image.xml') """ fmt_obj = None if annotation_format == 'base': return self.regions if annotation_format == 'json': return self.__format2base(file_path) elif annotation_format.lower() == 'array' or annotation_format.lower() == 'numpy' or annotation_format.lower() == 'mask': return self.__format2mask(file_path)[1] #elif annotation_format.lower() == 'vott': # return self.__format2vott(file_path) elif annotation_format.lower() == 'coco': return self.__format2coco(file_path) elif annotation_format.lower() == 'voc': return self.__format2voc(file_path) else: raise ValueError('Unsupported format')
def __calc_area_from_polygon(self, polygon): x = polygon[:, 0] y = polygon[:, 1] return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) def __format2base(self, output_fpath=None): if output_fpath is None: return self.regions else: with open(output_fpath, 'w', encoding='utf-8') as fh: json.dump(self.regions, fh, cls=JsonEncoder, ensure_ascii=False, indent=4) def __format2voc(self, output_fpath=None): tmpl_voc = '''<annotation> <folder></folder> <filename>{}</filename> <source> <database>Unknown</database> <annotation>Unknown</annotation> <image>Unknown</image> </source> <size> <width>{}</width> <height>{}</height> <depth></depth> </size> <segmented>0</segmented> {} </annotation> ''' tmpl_obj = ''' <object> <name>{}</name> <occluded>0</occluded> <bndbox> <xmin>{}</xmin> <ymin>{}</ymin> <xmax>{}</xmax> <ymax>{}</ymax> </bndbox> </object>''' objs = [] for region in self.regions: obj = tmpl_obj.format(region['class'], *region['bbox']) objs.append(obj) voc = tmpl_voc.format(os.path.basename(self.image_path), self.image.shape[1], self.image.shape[0], '\n'.join(objs)) if output_fpath is None: return voc else: with open(output_fpath, 'w', encoding='utf-8') as fh: fh.write(voc) def __format2coco(self, output_fpath=None): image_id = 1 tmpl = { 'images': [{ 'id': image_id, 'width': self.image.shape[1], 'height': self.image.shape[0], 'file_name': os.path.basename(self.image_path) }], 'annotations': [], 'categories': [] } # category cate2id = {} max_cate_id = 1 for region in self.regions: if region['class'] == '__background__': continue if region['class'] not in cate2id: cate2id[region['class']] = max_cate_id tmpl['categories'].append({ 'id': max_cate_id, 'name': region['class'], 'supercategory': region['class'] }) max_cate_id += 1 # annotations for i, region in enumerate(self.regions): if region['class'] == '__background__': continue ann = { 'id': i + 1, 'image_id': image_id, 'category_id': cate2id[region['class']], 'bbox': [region['bbox'][0], region['bbox'][1], region['bbox'][2] - region['bbox'][0], region['bbox'][3] - region['bbox'][1]], 'area': (region['bbox'][2] - region['bbox'][0]) * (region['bbox'][3] - region['bbox'][1]), 'iscrowd': 0 } if 'contour' in region and region['contour'] is not None: ann['segmentation'] = [region['contour'].flatten(order='C').tolist()] ann['area'] = self.__calc_area_from_polygon(region['contour']) tmpl['annotations'].append(ann) if output_fpath is None: return tmpl else: with open(output_fpath, 'w', encoding='utf-8') as fh: json.dump(tmpl, fh, cls=JsonEncoder, ensure_ascii=False, indent=4) def __format2vott(self, output_fpath=None): tmpl = {} # asset tmpl['asset'] = {} tmpl['asset']['format'] = os.path.splitext(self.image_path)[1].lower()[1:] tmpl['asset']['id'] = hashlib.md5('file:{}'.format(os.path.abspath(self.image_path)).encode('utf-8')).hexdigest() tmpl['asset']['name'] = os.path.basename(self.image_path) tmpl['asset']['path'] = 'file:{}'.format(os.path.abspath(self.image_path)) tmpl['asset']['size'] = {'width': self.image.shape[1], 'height': self.image.shape[0]} tmpl['asset']['state'] = 0 tmpl['asset']['type'] = 1 tmpl['version'] = '2.2.0' # regions tmpl['regions'] = [] for region in self.regions: r = {} r['id'] = region['id'] r['type'] = 'POLYGON' r['tags'] = region['class'] r['boundingBox'] = { 'height': region['bbox'][2] - region['bbox'][0] + 1, 'width': region['bbox'][3] - region['bbox'][1] + 1, 'left': region['bbox'][1], 'top': region['bbox'][0] } if region['contour'] is not None: r['points'] = [] for i in range(region['contour'].shape[0]): r['points'].append({'x': region['contour'][i, 1], 'y': region['contour'][i, 0]}) tmpl['regions'].append(r) if output_fpath is None: return tmpl else: if os.path.basename(output_fpath) == 'md5': with open(os.path.join(os.path.dirname(output_fpath), tmpl['asset']['id'] + '-asset.json'), 'w', encoding='utf-8') as fh: json.dump(tmpl, fh, cls=JsonEncoder, ensure_ascii=False, indent=4) elif os.path.splitext(output_fpath)[1] in ['.gzip', '.gz']: with gzip.open(output_fpath, 'wt', encoding='utf-8') as fh: json.dump(tmpl, fh, cls=JsonEncoder, ensure_ascii=False, indent=4) else: with open(output_fpath, 'w', encoding='utf-8') as fh: json.dump(tmpl, fh, cls=JsonEncoder, ensure_ascii=False, indent=4) def __format2mask(self, output_fpath=None): if self.mask is None: mask = [] for region_i, region in enumerate(self.regions): # mask for current region tmpl = np.zeros(self.image.shape[0:2]) if 'contour' in region and region['contour'] is not None: rr, cc = skimage.draw.polygon(region['contour'][:, 1], region['contour'][:, 0], shape=(self.image.shape[0:2])) else: rr, cc = skimage.draw.rectangle((region['bbox'][1], region['bbox'][0]), end=(region['bbox'][3], region['bbox'][2]), shape=(self.image.shape[0:2])) rr = rr.astype(np.int32) cc = cc.astype(np.int32) if region['class'] == '__background__': tmpl[rr, cc] = -1 else: # tmpl[rr, cc] = region['id'] tmpl[rr, cc] = 1 mask.append(tmpl) if len(mask) > 0: mask = np.array(mask).transpose(1, 2, 0) else: mask = np.zeros((self.image.shape[0], self.image.shape[1], 1)) self.mask = mask if output_fpath is None: return self.mask.copy() else: np.save(self.mask, output_fpath)
[docs] def draw(self, fig_type='mask', file_path=None, label=False, score=False, alpha=1.0, class2rgb=None): """Plot an image with annotations Method :func:`draw <justdeepit.utils.ImageAnnotation.draw>` depicts an image with annotations of class object :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>`. The output type can be specified by argument ``fig_type``, which can be specified as ``mask``, ``rgbmask``, ``masked``, ``bbox``, or ``contour``. Type ``mask`` plots a mask image, whose background is shown in black and the objects are shown in white. Type ``rgbmask`` plots an RGB mask image, where the background is shown in black while the objects are shown with different colors, unlike type mask. Objects belonging to the same class are plotted with the same colors according to ``class2rgb``. Type ``masked`` plots masked images with black background while the object area is the same as that of the original images. Type ``bbox`` plots an image with bounding boxes around objects. Type ``contour`` plots the contours of objects. In addition, multiple types can be specified simultaneously, such as ``mask+bbox`` and ``contour+bbox``. Args: fig_type (str): A string to specify the figure type to be plotted. One of ``mask``, ``rgbmask``, ``masked``, ``bbox``, or ``contour`` can be specified. file_path (str): A path to save the converted annotation. If ``None`` is given, return the image in :class:`numpy.ndarray` object. label (bool): Show class label of object on the image. score (bool): Show confidence score of object on the image if the score is not ``None``. alpha (float): A decimal number between 0.0 and 1.0 to specify the transparence of mask. class2rgb(dict): A dictionary with a key as a class name and value as a RGB value. If ``None`` is given, the preset colors will be used. Returns: If ``file_path`` is ``None``, return the image data in :class:`numpy.ndarray` object. Otherwise, save the data in the given path. Examples: >>> import matplotlib.pyplot as plt >>> from justdeepit.utils import ImageAnnotation >>> >>> image_fpath = './path/to/image.jpg' >>> ann_fpath = './path/to/image.json' >>> >>> ann = justdeepit.utils.ImageAnnotation(image_fpath, ann_fpath) >>> >>> ann.draw('bbox') """ if class2rgb is not None: self.class2rgb.update(class2rgb) img = None if fig_type == 'rgbmask': img = self.__draw_rgbmask() elif fig_type == 'mask': img = self.__draw_mask() elif fig_type == 'masked': img = self.__draw_maskedimage() elif fig_type == 'bbox': img = self.__draw_bbox() elif fig_type == 'contour': img = self.__draw_contours() elif 'bbox' in fig_type and 'rgbmask' in fig_type: img = self.__draw_rgbmask() img = self.__draw_bbox(img) elif 'bbox' in fig_type and 'masked' in fig_type: img = self.__draw_maskedimage() img = self.__draw_bbox(img) elif 'bbox' in fig_type and 'contour' in fig_type: img = self.__draw_contours() img = self.__draw_bbox(img) else: raise ValueError('Unsupported figure type: {}.'.format(fig_type)) if len(img.shape) == 2: _ = np.zeros((img.shape[0], img.shape[1], 3)) _[:, :, 0] = img.copy() _[:, :, 1] = img.copy() _[:, :, 2] = img.copy() img = _ img = img[:, :, :3] if alpha < 1.0: img = img * alpha + self.image[:, :, :3] * (1 - alpha) img = img.astype(np.uint8) img = self.__add_labels(img, label, score, fig_type) if self.image.shape[2] > 3: _ = np.zeros((self.image.shape[0], self.image.shape[1], self.image.shape[2] - 3)) _[:, :, :] = self.image[:, :, 3:] img = np.concatenate([img, _], axis=2) if file_path is None: return img else: skimage.io.imsave(file_path, img.astype(np.uint8), check_contrast=False)
def __put_text_cv(self, img, text, pos=(0, 0), font=cv2.FONT_HERSHEY_SIMPLEX, font_scale=1, font_thickness=2, color=(0, 255, 0), bg_color=(0, 0, 0)): ''' Write a text into image with OpenCV methods. ''' x, y = pos text_size, _ = cv2.getTextSize(text, font, font_scale, font_thickness) text_w, text_h = text_size cv2.rectangle(img, pos, (x + text_w + 10, y + text_h + 10), bg_color, -1) cv2.putText(img, text, (x + 5, y + text_h + font_scale + 5), font, font_scale, color, font_thickness) return img def __pil2cv(self, img): img_cv_rgb = np.array(img, dtype = np.uint8) img_cv_bgr = np.array(img)[:, :, ::-1] return img_cv_bgr def __cv2pil(self, img): return PIL.Image.fromarray(img[:, :, ::-1]) def __put_text(self, img, text, pos=(0, 0), font=None, font_scale=28, font_thickness=1, color=(0, 255, 0), bg_color=(0, 0, 0)): x, y = pos x = x + 5 y = y - font_scale - 5 if font is None: font = pkg_resources.resource_filename('justdeepit', 'src/font/NotoSans-Medium.ttf') img_pil = self.__cv2pil(img) font = PIL.ImageFont.truetype(font=font, size=font_scale) draw = PIL.ImageDraw.Draw(img_pil) l_, t_, r_, b_ = draw.textbbox((x, y), text, font=font) draw.rectangle((l_ - 5, t_ - 5, r_ + 5, b_ + 5), fill=color[::-1], outline=color[::-1], width=5) draw.text((x, y), text, font=font, fill=bg_color[::-1]) return self.__pil2cv(img_pil) def __get_bg_color(self, col): lbg = 0.2126 * col[0] + 0.7152 * col[1] + 0.0722 * col[2] lw = 1 lb = 0 cw = (lw + 0.05) / (lbg + 0.05) cb = (lbg + 0.05) / (lb + 0.05) bg = None if cb > cw: bg = (0, 0, 0) else: bg = (255, 255, 255) return bg def __get_line_weight(self, img_shape): lw = 2 min_edge = min(img_shape[0], img_shape[1]) if min_edge > 4068: lw = 6 if min_edge > 2048: lw = 5 elif min_edge > 1024: lw = 4 elif min_edge > 640: lw = 3 return lw def __add_labels(self, img, label=True, score=True, fig_type='', pos=None): self.__update_class2rgb() for region in self.regions: if region['class'] == '__background__': continue obj_label = '' if label: obj_label += region['class'] if score and (not np.isnan(region['score'])): obj_label += ' ({:.03f})'.format(region['score']) if obj_label != '': cx = cy = None if 'bbox' in fig_type: cx = int(region['bbox'][0]) cy = int(region['bbox'][1]) else: if 'contour' in region and region['contour'] is not None: m = cv2.moments(region['contour'].astype(np.int32)) cx = int(m['m10'] / m['m00']) cy = int(m['m01'] / m['m00']) if cx is not None and cy is not None: img = self.__put_text(img, obj_label, (cx, cy), color=self.class2rgb[region['class']], bg_color=self.__get_bg_color(self.class2rgb[region['class']])) return img def __draw_mask(self): mask = self.__format2mask() is_obj = np.sum(mask, axis=2) > 0 is_bg = np.sum(mask, axis=2) < 0 bimask = np.zeros((mask.shape[0], mask.shape[1], 3)) for ch in range(bimask.shape[2]): bimask[:, :, ch][is_obj] = 255 bimask[:, :, ch][is_bg] = 0 return bimask def __draw_rgbmask(self): mask = self.__format2mask() self.__update_class2rgb() bimask = self.__draw_mask() mask_rgb = np.zeros((mask.shape[0], mask.shape[1], 3)) for i in range(len(self.regions)): if self.regions[i]['class'] != '__background__': for ch in range(mask_rgb.shape[2]): mask_rgb[:, :, ch][mask[:, :, i] > 0] = self.class2rgb[self.regions[i]['class']][ch] for ch in range(mask_rgb.shape[2]): mask_rgb[:, :, ch] = mask_rgb[:, :, ch] * (bimask[:, :, 0] > 0).astype(np.uint8) return mask_rgb def __draw_maskedimage(self, base_image=None): mask = self.__format2mask() bimask = self.__draw_mask() if base_image is None: img = self.image.copy() else: img = base_image.copy() for ch in range(img.shape[2]): img[:, :, ch] = img[:, :, ch] * (bimask[:, :, 0] > 0).astype(np.uint8) return img def __draw_bbox(self, base_image=None): if base_image is None: img = self.image.copy() else: img = base_image.copy() line_bold = self.__get_line_weight(img.shape) self.__update_class2rgb() for region in self.regions: if region['class'] != '__background__': bbox = [int(_) for _ in region['bbox']] img = cv2.rectangle(img, bbox[0:2], bbox[2:4], color=self.class2rgb[region['class']], thickness=line_bold) return img def __draw_contours(self, base_image=None): if base_image is None: img = self.image.copy() else: img = base_image.copy() line_bold = self.__get_line_weight(img.shape) self.__update_class2rgb() for region in self.regions: if 'contour' in region and region['contour'] is not None: contour = region['contour'].astype(np.int32) cv2.polylines(img, [contour], True, self.class2rgb[region['class']], thickness=line_bold) return img def __update_class2rgb(self): for region in self.regions: if region['class'] not in self.class2rgb: self.class2rgb[region['class']] = self.__rgb.pop() def __image2hash(self, image_fpath): with open(image_fpath, 'rb') as fh: image_data = base64.b64encode(fh.read()) image_hash = hashlib.md5(image_data.decode('ascii').encode('utf-8')).hexdigest() return image_hash def __generate_random_string(self, n): return ''.join(random.choices(string.ascii_letters + string.digits, k=n)) def __set_colormapping_dict(self, class2rgb, rgb2class): if (isinstance(class2rgb, (type(None), dict))) and (isinstance(rgb2class, (type(None), dict))): if (class2rgb is None) and (rgb2class is None): class2rgb = {} rgb2class = {} elif (class2rgb is not None) and (rgb2class is None): rgb2class = {} for cl, rgb in class2rgb.items(): rgb_str = ','.join([str(_) for _ in rgb]) rgb2class[rgb_str] = cl elif (class2rgb is None) and (rgb2class is not None): class2rgb = {} for rgb, cl in rgb2class.items(): class2rgb[cl] = [int(_) for _ in rgb.split(',')] elif (class2rgb is not None) and (rgb2class is not None): pass else: raise ValueError('Unexpected Error during setting color mapping dictionary.') else: raise ValueError('`class2rgb` and `rgb2class` arguments should be NoneType or given by a dictionary.') return class2rgb, rgb2class
[docs] class ImageAnnotations: """List of :class:`ImageAnnocation <justdeepit.utils.ImageAnnotation>` objects :class:`ImageAnnotations <justdeepit.utils.ImageAnnotations>` stores :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` class objects as a list object. To retrieve elements from an :class:`ImageAnnotations <justdeepit.utils.ImageAnnotationss>` object, indexing ``[i]`` and a ``for`` loop can be used. To add new elements to the end of :class:`ImageAnnotations <justdeepit.utils.ImageAnnotations>`, methods :func:`append <justdeepit.utils.ImageAnnotations.append>` and :func:`extend <justdeepit.utils.ImageAnnotations.extend>` can be used. In addition, this class implements method :func:`format <justdeepit.utils.ImageAnnotations.format>` to convert annotations into files in a specific format, such as COCO or Pascal VOC. Compared with :func:`format <justdeepit.utils.ImageAnnotation.format>` implemented in class :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` that generates annotations in a specific format for a single image, :class:`format <justdeepit.utils.ImageAnnotations.format>` in this class can generate annotations for multiple images. Args: x: If ``x`` is ``None``, generate an empty :class:`ImageAnnocations <justdeepit.utils.ImageAnnotations>` object. If ``x`` is :class:`ImageAnnocation <justdeepit.utils.ImageAnnotation>` or :class:`ImageAnnocations <justdeepit.utils.ImageAnnotations>` object, convert them to :class:`ImageAnnocations <justdeepit.utils.ImageAnnotations>` object. Examples: >>> from justdeepit.utils import ImageAnnotation, ImageAnnotations >>> ann_1 = ImageAnnotation('image_1.jpg', 'image_1.xml') >>> ann_2 = ImageAnnotation('image_2.jpg', 'image_2.xml') >>> ann_3 = ImageAnnotation('image_3.jpg', 'image_3.xml') >>> ann = justdeepit.utils.ImageAnnotations([ann_1, ann_2, ann_3]) """ def __init__(self, x=None): self.image_annotations = [] if x is None: pass elif isinstance(x, ImageAnnotation): self.image_annotations.append(x) elif isinstance(x, list): for i in x: if isinstance(i, ImageAnnotation): self.image_annotations.append(i) else: raise ValueError('Unable to parse the input objects.') elif isinstance(x, ImageAnnotations): self.image_annotations = x.image_annotations else: raise ValueError('Unable to parse the input objects.') self.i = 0 def __iter__(self): return self def __next__(self): try: return self.image_annotations[self.i] except IndexError: self.i = 0 raise StopIteration() finally: self.i += 1 def __len__(self): return len(self.image_annotations) def __getitem__(self, key): return self.image_annotations[key] def __setitem__(self, key, value): self.image_annotations[key] = value
[docs] def append(self, image_annotation): """Add an :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` object to the end of the :class:`ImageAnnotations <justdeepit.utils.ImageAnnotations>` object Add an :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` object to the end of the :class:`ImageAnnotations <justdeepit.utils.ImageAnnotations>` object. Args: image_annotation (ImageAnnotation): An :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` object. Examples: >>> import glob >>> from justdeepit.utils import ImageAnnotation, ImageAnnotations >>> >>> anns = ImageAnnotations() >>> >>> for image_fpath in glob.glob(os.path.join(images_dpath, '*.jpg')): >>> ann_fpath = os.split.ext(image_fpath)[0] + '.xml' >>> ann = ImageAnnotation(image_fpath, ann_fpath, annotation_format='voc') >>> anns.append(ann) >>> >>> anns.fomrat('COCO', file_path='./images_annotation.json') """ assert type(image_annotation).__name__ == 'ImageAnnotation', 'Inputs should be ImageAnnotation class instance.' self.image_annotations.append(image_annotation)
[docs] def extend(self, image_annotations): """Concatenate :class:`ImageAnnotations <justdeepit.utils.ImageAnnotations>` object to the end of the :class:`ImageAnnotations <justdeepit.utils.ImageAnnotations>` object Concatenate :class:`ImageAnnotations <justdeepit.utils.ImageAnnotations>` object or a list of :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` objects to the end of the :class:`ImageAnnotations <justdeepit.utils.ImageAnnotations>` object. Args: image_annotations (ImageAnnotations, list): An :class:`ImageAnnotations <justdeepit.utils.ImageAnnotations>` object or a list of :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>` objects. Examples: >>> import glob >>> from justdeepit.utils import ImageAnnotation, ImageAnnotations >>> >>> anns_1 = ImageAnnotations() >>> for image_fpath in glob.glob(os.path.join('subset_1', '*.jpg')): >>> ann_fpath = os.split.ext(image_fpath)[0] + '.xml' >>> ann = ImageAnnotation(image_fpath, ann_fpath, annotation_format='voc') >>> anns_1.append(ann) >>> >>> anns_2 = ImageAnnotations() >>> for image_fpath in glob.glob(os.path.join('subset_2', '*.jpg')): >>> ann_fpath = os.split.ext(image_fpath)[0] + '.xml' >>> ann = ImageAnnotation(image_fpath, ann_fpath, annotation_format='voc') >>> anns_2.append(ann) >>> >>> >>> anns = ImageAnnotations() >>> anns.extend(anns_1) >>> anns.extend(anns_2) >>> anns.fomrat('COCO', file_path='./images_annotation.json') >>> """ for image_annotation in image_annotations: assert type(image_annotation).__name__ == 'ImageAnnotation', \ 'Inputs should be ImageAnnotation class instance.' self.image_annotations.append(image_annotation)
[docs] def format(self, annotation_format, file_path=None, dataset_name='dataset'): """Format annotaitons to specific format Rearrange :class:`ImageAnnotations <justdeepit.utils.ImageAnnotations>` object to a specific format. COCO (.json) and Pascal VOC (.xml) are supported in current version. Different to :func:`format <justdeepit.utils.ImageAnnotation.format>` implemented in :class:`ImageAnnotation <justdeepit.utils.ImageAnnotation>`, COCO format annotations of multiple images are saved into single JSON file. Args: annotation_format (str): A string to specify the format to be formatted. file_path (str): A path to save the converted annotation. If ``None`` is given, return the converted annotation in string or dictionary. dataset_name (str): Data set name. Some annotaiton requires dataset name. Returns: If ``file_path`` is ``None``, return a string (for Pascal VOC format), a dictionary (for COCO), or :class:`numpy.ndarray` (for image data). Otherwise, save the data in the given path. Examples: >>> import glob >>> from justdeepit.utils import ImageAnnotation, ImageAnnotations >>> >>> anns = ImageAnnotations() >>> >>> for image_fpath in glob.glob(os.path.join(images_dpath, '*.jpg')): >>> ann_fpath = os.split.ext(image_fpath)[0] + '.xml' >>> ann = ImageAnnotation(image_fpath, ann_fpath, annotation_format='voc') >>> anns.append(ann) >>> >>> anns.fomrat('COCO', file_path='./images_annotation.json') """ if file_path is None: raise FileNotFoundError('file_path cannot be empty, set file_path to save the annotation.') if annotation_format == 'base': for image_annotation in self.image_annotations: fpath = os.path.join(file_path, os.path.splitext(os.path.basename(image_annotation.image_path))[0] + '.json') annotation_format.format('base', fpath) elif annotation_format.lower() == 'array' or annotation_format.lower() == 'numpy' or annotation_format.lower() == 'mask': for image_annotation in self.image_annotations: fpath = os.path.join(file_path, os.path.splitext(os.path.basename(image_annotation.image_path))[0] + '.npy') annotation_format.format('mask', fpath) #elif annotation_format.lower() == 'vott': # self.__format2vott(file_path, dataset_name) elif annotation_format.lower() == 'coco': self.__format2coco(file_path, dataset_name) elif annotation_format.lower() == 'voc': for image_annotation in self.image_annotations: fpath = os.path.join(file_path, os.path.splitext(os.path.basename(image_annotation.image_path))[0] + '.xml') annotation_format.format('voc', fpath) else: raise ValueError('Unsupported foramt.')
def __format2coco(self, output_fpath, dataset_name): tmpl = { 'images': [], 'annotations': [], 'categories': [] } tag2id = {} tag_ = [] for imann in self.image_annotations: for region in imann.regions: tag_.append(region['class']) for tag_id, tag_name in enumerate(sorted(list(set(tag_)))): tag2id[tag_name] = tag_id + 1 max_ann_id = 1 for img_id, imann in enumerate(self.image_annotations): img_id += 1 imann_coco = imann.format('coco') # check tags id2tag = {} for cate_ in imann_coco['categories']: id2tag[str(cate_['id'])] = cate_['name'] # update image info imann_coco['images'][0]['id'] = img_id # update annotations for ann_id in range(len(imann_coco['annotations'])): imann_coco['annotations'][ann_id]['id'] = max_ann_id imann_coco['annotations'][ann_id]['image_id'] = img_id imann_coco['annotations'][ann_id]['category_id'] = tag2id[id2tag[str(imann_coco['annotations'][ann_id]['category_id'])]] max_ann_id += 1 tmpl['images'].append(imann_coco['images'][0]) tmpl['annotations'].extend(imann_coco['annotations']) for tag_name, tag_id in tag2id.items(): tmpl['categories'].append({ 'id': tag_id, 'name': tag_name, 'supercategory': tag_name }) with open(output_fpath, 'w', encoding='utf-8') as fh: json.dump(tmpl, fh, cls=JsonEncoder, ensure_ascii=False, indent=2) def __format2vott(self, output_fpath, dataset_name): tmpl = { 'name': dataset_name, 'securityToken': '', 'sourceConnection': { 'name': dataset_name, 'providerType': 'localFileSystemProxy', 'providerOptions': { 'encrypted': '' }, 'id': '' }, 'targetConnection': { 'name': dataset_name, 'providerType': 'localFileSystemProxy', 'providerOptions': { 'encrypted': '' }, 'id': '' }, 'videoSettings': { 'frameExtractionRate': 15 }, 'tags': [], 'id': '', 'activeLearningSettings': { 'autoDetect': False, 'predictTag': True, 'modelPathType': 'coco' }, 'exportFormat': { 'providerType': 'vottJson', 'providerOptions': { 'encrypted': '' } }, 'version': '2.2.0', 'lastVisitedAssetId': '', 'assets': {} }
#tags = set() #for imann in self.image_annotations: # tmpl_ = imann.format('vott') # tmpl.assets[tmpl_['asset']['id']] = tmpl_['asset'] # for region in tmpl_['regions']: # tags.add(region['tags'].split(';')) #for tag in tags: # tmpl['tags'].append({'name': tag, 'color': '#0000FF'}) # save project.vott # save assets.json def load_images(images): image_list = [] if isinstance(images, list): image_list = images elif os.path.isfile(images): image_list = [images] else: for f in glob.glob(os.path.join(images, '*')): if os.path.splitext(f)[1].lower() in ['.jpg', '.png', '.tiff']: image_list.append(f) return image_list