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
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)
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