|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""A set of functions that are used for visualization. |
|
|
|
These functions often receive an image, perform some visualization on the image. |
|
The functions do not return a value, instead they modify the image itself. |
|
|
|
""" |
|
import abc |
|
import collections |
|
import functools |
|
|
|
import matplotlib; matplotlib.use('Agg') |
|
import matplotlib.pyplot as plt |
|
import numpy as np |
|
import PIL.Image as Image |
|
import PIL.ImageColor as ImageColor |
|
import PIL.ImageDraw as ImageDraw |
|
import PIL.ImageFont as ImageFont |
|
import six |
|
import tensorflow as tf |
|
|
|
from object_detection.core import standard_fields as fields |
|
from object_detection.utils import shape_utils |
|
|
|
_TITLE_LEFT_MARGIN = 10 |
|
_TITLE_TOP_MARGIN = 10 |
|
STANDARD_COLORS = [ |
|
'AliceBlue', 'Chartreuse', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', |
|
'BlanchedAlmond', 'BlueViolet', 'BurlyWood', 'CadetBlue', 'AntiqueWhite', |
|
'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', |
|
'DarkCyan', 'DarkGoldenRod', 'DarkGrey', 'DarkKhaki', 'DarkOrange', |
|
'DarkOrchid', 'DarkSalmon', 'DarkSeaGreen', 'DarkTurquoise', 'DarkViolet', |
|
'DeepPink', 'DeepSkyBlue', 'DodgerBlue', 'FireBrick', 'FloralWhite', |
|
'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', |
|
'Salmon', 'Tan', 'HoneyDew', 'HotPink', 'IndianRed', 'Ivory', 'Khaki', |
|
'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', |
|
'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', |
|
'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', |
|
'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', |
|
'LimeGreen', 'Linen', 'Magenta', 'MediumAquaMarine', 'MediumOrchid', |
|
'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', |
|
'MediumTurquoise', 'MediumVioletRed', 'MintCream', 'MistyRose', 'Moccasin', |
|
'NavajoWhite', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', |
|
'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', |
|
'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', |
|
'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Green', 'SandyBrown', |
|
'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', |
|
'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'GreenYellow', |
|
'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', |
|
'WhiteSmoke', 'Yellow', 'YellowGreen' |
|
] |
|
|
|
|
|
def save_image_array_as_png(image, output_path): |
|
"""Saves an image (represented as a numpy array) to PNG. |
|
|
|
Args: |
|
image: a numpy array with shape [height, width, 3]. |
|
output_path: path to which image should be written. |
|
""" |
|
image_pil = Image.fromarray(np.uint8(image)).convert('RGB') |
|
with tf.gfile.Open(output_path, 'w') as fid: |
|
image_pil.save(fid, 'PNG') |
|
|
|
|
|
def encode_image_array_as_png_str(image): |
|
"""Encodes a numpy array into a PNG string. |
|
|
|
Args: |
|
image: a numpy array with shape [height, width, 3]. |
|
|
|
Returns: |
|
PNG encoded image string. |
|
""" |
|
image_pil = Image.fromarray(np.uint8(image)) |
|
output = six.BytesIO() |
|
image_pil.save(output, format='PNG') |
|
png_string = output.getvalue() |
|
output.close() |
|
return png_string |
|
|
|
|
|
def draw_bounding_box_on_image_array(image, |
|
ymin, |
|
xmin, |
|
ymax, |
|
xmax, |
|
color='red', |
|
thickness=4, |
|
display_str_list=(), |
|
use_normalized_coordinates=True): |
|
"""Adds a bounding box to an image (numpy array). |
|
|
|
Bounding box coordinates can be specified in either absolute (pixel) or |
|
normalized coordinates by setting the use_normalized_coordinates argument. |
|
|
|
Args: |
|
image: a numpy array with shape [height, width, 3]. |
|
ymin: ymin of bounding box. |
|
xmin: xmin of bounding box. |
|
ymax: ymax of bounding box. |
|
xmax: xmax of bounding box. |
|
color: color to draw bounding box. Default is red. |
|
thickness: line thickness. Default value is 4. |
|
display_str_list: list of strings to display in box |
|
(each to be shown on its own line). |
|
use_normalized_coordinates: If True (default), treat coordinates |
|
ymin, xmin, ymax, xmax as relative to the image. Otherwise treat |
|
coordinates as absolute. |
|
""" |
|
image_pil = Image.fromarray(np.uint8(image)).convert('RGB') |
|
draw_bounding_box_on_image(image_pil, ymin, xmin, ymax, xmax, color, |
|
thickness, display_str_list, |
|
use_normalized_coordinates) |
|
np.copyto(image, np.array(image_pil)) |
|
|
|
|
|
def draw_bounding_box_on_image(image, |
|
ymin, |
|
xmin, |
|
ymax, |
|
xmax, |
|
color='red', |
|
thickness=4, |
|
display_str_list=(), |
|
use_normalized_coordinates=True): |
|
"""Adds a bounding box to an image. |
|
|
|
Bounding box coordinates can be specified in either absolute (pixel) or |
|
normalized coordinates by setting the use_normalized_coordinates argument. |
|
|
|
Each string in display_str_list is displayed on a separate line above the |
|
bounding box in black text on a rectangle filled with the input 'color'. |
|
If the top of the bounding box extends to the edge of the image, the strings |
|
are displayed below the bounding box. |
|
|
|
Args: |
|
image: a PIL.Image object. |
|
ymin: ymin of bounding box. |
|
xmin: xmin of bounding box. |
|
ymax: ymax of bounding box. |
|
xmax: xmax of bounding box. |
|
color: color to draw bounding box. Default is red. |
|
thickness: line thickness. Default value is 4. |
|
display_str_list: list of strings to display in box |
|
(each to be shown on its own line). |
|
use_normalized_coordinates: If True (default), treat coordinates |
|
ymin, xmin, ymax, xmax as relative to the image. Otherwise treat |
|
coordinates as absolute. |
|
""" |
|
draw = ImageDraw.Draw(image) |
|
im_width, im_height = image.size |
|
if use_normalized_coordinates: |
|
(left, right, top, bottom) = (xmin * im_width, xmax * im_width, |
|
ymin * im_height, ymax * im_height) |
|
else: |
|
(left, right, top, bottom) = (xmin, xmax, ymin, ymax) |
|
draw.line([(left, top), (left, bottom), (right, bottom), |
|
(right, top), (left, top)], width=thickness, fill=color) |
|
try: |
|
font = ImageFont.truetype('arial', 90) |
|
except IOError: |
|
font = ImageFont.load_default() |
|
|
|
|
|
|
|
|
|
display_str_heights = [font.getsize(ds)[1] for ds in display_str_list] |
|
|
|
total_display_str_height = (1 + 2 * 0.05) * sum(display_str_heights) |
|
|
|
if top > total_display_str_height: |
|
text_bottom = top |
|
else: |
|
text_bottom = bottom + total_display_str_height |
|
|
|
for display_str in display_str_list[::-1]: |
|
text_width, text_height = font.getsize(display_str) |
|
margin = np.ceil(0.05 * text_height) |
|
draw.rectangle( |
|
[(left, text_bottom - text_height - 2 * margin), (left + text_width, |
|
text_bottom)], |
|
fill=color) |
|
draw.text( |
|
(left + margin, text_bottom - text_height - margin), |
|
display_str, |
|
fill='black', |
|
font=font) |
|
text_bottom -= text_height - 2 * margin |
|
|
|
|
|
def draw_bounding_boxes_on_image_array(image, |
|
boxes, |
|
color='red', |
|
thickness=4, |
|
display_str_list_list=()): |
|
"""Draws bounding boxes on image (numpy array). |
|
|
|
Args: |
|
image: a numpy array object. |
|
boxes: a 2 dimensional numpy array of [N, 4]: (ymin, xmin, ymax, xmax). |
|
The coordinates are in normalized format between [0, 1]. |
|
color: color to draw bounding box. Default is red. |
|
thickness: line thickness. Default value is 4. |
|
display_str_list_list: list of list of strings. |
|
a list of strings for each bounding box. |
|
The reason to pass a list of strings for a |
|
bounding box is that it might contain |
|
multiple labels. |
|
|
|
Raises: |
|
ValueError: if boxes is not a [N, 4] array |
|
""" |
|
image_pil = Image.fromarray(image) |
|
draw_bounding_boxes_on_image(image_pil, boxes, color, thickness, |
|
display_str_list_list) |
|
np.copyto(image, np.array(image_pil)) |
|
|
|
|
|
def draw_bounding_boxes_on_image(image, |
|
boxes, |
|
color='red', |
|
thickness=4, |
|
display_str_list_list=()): |
|
"""Draws bounding boxes on image. |
|
|
|
Args: |
|
image: a PIL.Image object. |
|
boxes: a 2 dimensional numpy array of [N, 4]: (ymin, xmin, ymax, xmax). |
|
The coordinates are in normalized format between [0, 1]. |
|
color: color to draw bounding box. Default is red. |
|
thickness: line thickness. Default value is 4. |
|
display_str_list_list: list of list of strings. |
|
a list of strings for each bounding box. |
|
The reason to pass a list of strings for a |
|
bounding box is that it might contain |
|
multiple labels. |
|
|
|
Raises: |
|
ValueError: if boxes is not a [N, 4] array |
|
""" |
|
boxes_shape = boxes.shape |
|
if not boxes_shape: |
|
return |
|
if len(boxes_shape) != 2 or boxes_shape[1] != 4: |
|
raise ValueError('Input must be of size [N, 4]') |
|
for i in range(boxes_shape[0]): |
|
display_str_list = () |
|
if display_str_list_list: |
|
display_str_list = display_str_list_list[i] |
|
draw_bounding_box_on_image(image, boxes[i, 0], boxes[i, 1], boxes[i, 2], |
|
boxes[i, 3], color, thickness, display_str_list) |
|
|
|
|
|
def _visualize_boxes(image, boxes, classes, scores, category_index, **kwargs): |
|
return visualize_boxes_and_labels_on_image_array( |
|
image, boxes, classes, scores, category_index=category_index, **kwargs) |
|
|
|
|
|
def _visualize_boxes_and_masks(image, boxes, classes, scores, masks, |
|
category_index, **kwargs): |
|
return visualize_boxes_and_labels_on_image_array( |
|
image, |
|
boxes, |
|
classes, |
|
scores, |
|
category_index=category_index, |
|
instance_masks=masks, |
|
**kwargs) |
|
|
|
|
|
def _visualize_boxes_and_keypoints(image, boxes, classes, scores, keypoints, |
|
category_index, **kwargs): |
|
return visualize_boxes_and_labels_on_image_array( |
|
image, |
|
boxes, |
|
classes, |
|
scores, |
|
category_index=category_index, |
|
keypoints=keypoints, |
|
**kwargs) |
|
|
|
|
|
def _visualize_boxes_and_masks_and_keypoints( |
|
image, boxes, classes, scores, masks, keypoints, category_index, **kwargs): |
|
return visualize_boxes_and_labels_on_image_array( |
|
image, |
|
boxes, |
|
classes, |
|
scores, |
|
category_index=category_index, |
|
instance_masks=masks, |
|
keypoints=keypoints, |
|
**kwargs) |
|
|
|
|
|
def _resize_original_image(image, image_shape): |
|
image = tf.expand_dims(image, 0) |
|
image = tf.image.resize_images( |
|
image, |
|
image_shape, |
|
method=tf.image.ResizeMethod.NEAREST_NEIGHBOR, |
|
align_corners=True) |
|
return tf.cast(tf.squeeze(image, 0), tf.uint8) |
|
|
|
|
|
def draw_bounding_boxes_on_image_tensors(images, |
|
boxes, |
|
classes, |
|
scores, |
|
category_index, |
|
original_image_spatial_shape=None, |
|
true_image_shape=None, |
|
instance_masks=None, |
|
keypoints=None, |
|
max_boxes_to_draw=20, |
|
min_score_thresh=0.2, |
|
use_normalized_coordinates=True): |
|
"""Draws bounding boxes, masks, and keypoints on batch of image tensors. |
|
|
|
Args: |
|
images: A 4D uint8 image tensor of shape [N, H, W, C]. If C > 3, additional |
|
channels will be ignored. If C = 1, then we convert the images to RGB |
|
images. |
|
boxes: [N, max_detections, 4] float32 tensor of detection boxes. |
|
classes: [N, max_detections] int tensor of detection classes. Note that |
|
classes are 1-indexed. |
|
scores: [N, max_detections] float32 tensor of detection scores. |
|
category_index: a dict that maps integer ids to category dicts. e.g. |
|
{1: {1: 'dog'}, 2: {2: 'cat'}, ...} |
|
original_image_spatial_shape: [N, 2] tensor containing the spatial size of |
|
the original image. |
|
true_image_shape: [N, 3] tensor containing the spatial size of unpadded |
|
original_image. |
|
instance_masks: A 4D uint8 tensor of shape [N, max_detection, H, W] with |
|
instance masks. |
|
keypoints: A 4D float32 tensor of shape [N, max_detection, num_keypoints, 2] |
|
with keypoints. |
|
max_boxes_to_draw: Maximum number of boxes to draw on an image. Default 20. |
|
min_score_thresh: Minimum score threshold for visualization. Default 0.2. |
|
use_normalized_coordinates: Whether to assume boxes and kepoints are in |
|
normalized coordinates (as opposed to absolute coordiantes). |
|
Default is True. |
|
|
|
Returns: |
|
4D image tensor of type uint8, with boxes drawn on top. |
|
""" |
|
|
|
if images.shape[3] > 3: |
|
images = images[:, :, :, 0:3] |
|
elif images.shape[3] == 1: |
|
images = tf.image.grayscale_to_rgb(images) |
|
visualization_keyword_args = { |
|
'use_normalized_coordinates': use_normalized_coordinates, |
|
'max_boxes_to_draw': max_boxes_to_draw, |
|
'min_score_thresh': min_score_thresh, |
|
'agnostic_mode': False, |
|
'line_thickness': 4 |
|
} |
|
if true_image_shape is None: |
|
true_shapes = tf.constant(-1, shape=[images.shape.as_list()[0], 3]) |
|
else: |
|
true_shapes = true_image_shape |
|
if original_image_spatial_shape is None: |
|
original_shapes = tf.constant(-1, shape=[images.shape.as_list()[0], 2]) |
|
else: |
|
original_shapes = original_image_spatial_shape |
|
|
|
if instance_masks is not None and keypoints is None: |
|
visualize_boxes_fn = functools.partial( |
|
_visualize_boxes_and_masks, |
|
category_index=category_index, |
|
**visualization_keyword_args) |
|
elems = [ |
|
true_shapes, original_shapes, images, boxes, classes, scores, |
|
instance_masks |
|
] |
|
elif instance_masks is None and keypoints is not None: |
|
visualize_boxes_fn = functools.partial( |
|
_visualize_boxes_and_keypoints, |
|
category_index=category_index, |
|
**visualization_keyword_args) |
|
elems = [ |
|
true_shapes, original_shapes, images, boxes, classes, scores, keypoints |
|
] |
|
elif instance_masks is not None and keypoints is not None: |
|
visualize_boxes_fn = functools.partial( |
|
_visualize_boxes_and_masks_and_keypoints, |
|
category_index=category_index, |
|
**visualization_keyword_args) |
|
elems = [ |
|
true_shapes, original_shapes, images, boxes, classes, scores, |
|
instance_masks, keypoints |
|
] |
|
else: |
|
visualize_boxes_fn = functools.partial( |
|
_visualize_boxes, |
|
category_index=category_index, |
|
**visualization_keyword_args) |
|
elems = [ |
|
true_shapes, original_shapes, images, boxes, classes, scores |
|
] |
|
|
|
def draw_boxes(image_and_detections): |
|
"""Draws boxes on image.""" |
|
true_shape = image_and_detections[0] |
|
original_shape = image_and_detections[1] |
|
if true_image_shape is not None: |
|
image = shape_utils.pad_or_clip_nd(image_and_detections[2], |
|
[true_shape[0], true_shape[1], 3]) |
|
if original_image_spatial_shape is not None: |
|
image_and_detections[2] = _resize_original_image(image, original_shape) |
|
|
|
image_with_boxes = tf.py_func(visualize_boxes_fn, image_and_detections[2:], |
|
tf.uint8) |
|
return image_with_boxes |
|
|
|
images = tf.map_fn(draw_boxes, elems, dtype=tf.uint8, back_prop=False) |
|
return images |
|
|
|
|
|
def draw_side_by_side_evaluation_image(eval_dict, |
|
category_index, |
|
max_boxes_to_draw=20, |
|
min_score_thresh=0.2, |
|
use_normalized_coordinates=True): |
|
"""Creates a side-by-side image with detections and groundtruth. |
|
|
|
Bounding boxes (and instance masks, if available) are visualized on both |
|
subimages. |
|
|
|
Args: |
|
eval_dict: The evaluation dictionary returned by |
|
eval_util.result_dict_for_batched_example() or |
|
eval_util.result_dict_for_single_example(). |
|
category_index: A category index (dictionary) produced from a labelmap. |
|
max_boxes_to_draw: The maximum number of boxes to draw for detections. |
|
min_score_thresh: The minimum score threshold for showing detections. |
|
use_normalized_coordinates: Whether to assume boxes and kepoints are in |
|
normalized coordinates (as opposed to absolute coordiantes). |
|
Default is True. |
|
|
|
Returns: |
|
A list of [1, H, 2 * W, C] uint8 tensor. The subimage on the left |
|
corresponds to detections, while the subimage on the right corresponds to |
|
groundtruth. |
|
""" |
|
detection_fields = fields.DetectionResultFields() |
|
input_data_fields = fields.InputDataFields() |
|
|
|
images_with_detections_list = [] |
|
|
|
|
|
if len(eval_dict[detection_fields.detection_classes].shape) == 1: |
|
for key in eval_dict: |
|
if key != input_data_fields.original_image: |
|
eval_dict[key] = tf.expand_dims(eval_dict[key], 0) |
|
|
|
for indx in range(eval_dict[input_data_fields.original_image].shape[0]): |
|
instance_masks = None |
|
if detection_fields.detection_masks in eval_dict: |
|
instance_masks = tf.cast( |
|
tf.expand_dims( |
|
eval_dict[detection_fields.detection_masks][indx], axis=0), |
|
tf.uint8) |
|
keypoints = None |
|
if detection_fields.detection_keypoints in eval_dict: |
|
keypoints = tf.expand_dims( |
|
eval_dict[detection_fields.detection_keypoints][indx], axis=0) |
|
groundtruth_instance_masks = None |
|
if input_data_fields.groundtruth_instance_masks in eval_dict: |
|
groundtruth_instance_masks = tf.cast( |
|
tf.expand_dims( |
|
eval_dict[input_data_fields.groundtruth_instance_masks][indx], |
|
axis=0), tf.uint8) |
|
|
|
images_with_detections = draw_bounding_boxes_on_image_tensors( |
|
tf.expand_dims( |
|
eval_dict[input_data_fields.original_image][indx], axis=0), |
|
tf.expand_dims( |
|
eval_dict[detection_fields.detection_boxes][indx], axis=0), |
|
tf.expand_dims( |
|
eval_dict[detection_fields.detection_classes][indx], axis=0), |
|
tf.expand_dims( |
|
eval_dict[detection_fields.detection_scores][indx], axis=0), |
|
category_index, |
|
original_image_spatial_shape=tf.expand_dims( |
|
eval_dict[input_data_fields.original_image_spatial_shape][indx], |
|
axis=0), |
|
true_image_shape=tf.expand_dims( |
|
eval_dict[input_data_fields.true_image_shape][indx], axis=0), |
|
instance_masks=instance_masks, |
|
keypoints=keypoints, |
|
max_boxes_to_draw=max_boxes_to_draw, |
|
min_score_thresh=min_score_thresh, |
|
use_normalized_coordinates=use_normalized_coordinates) |
|
images_with_groundtruth = draw_bounding_boxes_on_image_tensors( |
|
tf.expand_dims( |
|
eval_dict[input_data_fields.original_image][indx], axis=0), |
|
tf.expand_dims( |
|
eval_dict[input_data_fields.groundtruth_boxes][indx], axis=0), |
|
tf.expand_dims( |
|
eval_dict[input_data_fields.groundtruth_classes][indx], axis=0), |
|
tf.expand_dims( |
|
tf.ones_like( |
|
eval_dict[input_data_fields.groundtruth_classes][indx], |
|
dtype=tf.float32), |
|
axis=0), |
|
category_index, |
|
original_image_spatial_shape=tf.expand_dims( |
|
eval_dict[input_data_fields.original_image_spatial_shape][indx], |
|
axis=0), |
|
true_image_shape=tf.expand_dims( |
|
eval_dict[input_data_fields.true_image_shape][indx], axis=0), |
|
instance_masks=groundtruth_instance_masks, |
|
keypoints=None, |
|
max_boxes_to_draw=None, |
|
min_score_thresh=0.0, |
|
use_normalized_coordinates=use_normalized_coordinates) |
|
images_with_detections_list.append( |
|
tf.concat([images_with_detections, images_with_groundtruth], axis=2)) |
|
return images_with_detections_list |
|
|
|
|
|
def draw_keypoints_on_image_array(image, |
|
keypoints, |
|
color='red', |
|
radius=2, |
|
use_normalized_coordinates=True): |
|
"""Draws keypoints on an image (numpy array). |
|
|
|
Args: |
|
image: a numpy array with shape [height, width, 3]. |
|
keypoints: a numpy array with shape [num_keypoints, 2]. |
|
color: color to draw the keypoints with. Default is red. |
|
radius: keypoint radius. Default value is 2. |
|
use_normalized_coordinates: if True (default), treat keypoint values as |
|
relative to the image. Otherwise treat them as absolute. |
|
""" |
|
image_pil = Image.fromarray(np.uint8(image)).convert('RGB') |
|
draw_keypoints_on_image(image_pil, keypoints, color, radius, |
|
use_normalized_coordinates) |
|
np.copyto(image, np.array(image_pil)) |
|
|
|
|
|
def draw_keypoints_on_image(image, |
|
keypoints, |
|
color='red', |
|
radius=2, |
|
use_normalized_coordinates=True): |
|
"""Draws keypoints on an image. |
|
|
|
Args: |
|
image: a PIL.Image object. |
|
keypoints: a numpy array with shape [num_keypoints, 2]. |
|
color: color to draw the keypoints with. Default is red. |
|
radius: keypoint radius. Default value is 2. |
|
use_normalized_coordinates: if True (default), treat keypoint values as |
|
relative to the image. Otherwise treat them as absolute. |
|
""" |
|
draw = ImageDraw.Draw(image) |
|
im_width, im_height = image.size |
|
keypoints_x = [k[1] for k in keypoints] |
|
keypoints_y = [k[0] for k in keypoints] |
|
if use_normalized_coordinates: |
|
keypoints_x = tuple([im_width * x for x in keypoints_x]) |
|
keypoints_y = tuple([im_height * y for y in keypoints_y]) |
|
for keypoint_x, keypoint_y in zip(keypoints_x, keypoints_y): |
|
draw.ellipse([(keypoint_x - radius, keypoint_y - radius), |
|
(keypoint_x + radius, keypoint_y + radius)], |
|
outline=color, fill=color) |
|
|
|
|
|
def draw_mask_on_image_array(image, mask, color='red', alpha=0.4): |
|
"""Draws mask on an image. |
|
|
|
Args: |
|
image: uint8 numpy array with shape (img_height, img_height, 3) |
|
mask: a uint8 numpy array of shape (img_height, img_height) with |
|
values between either 0 or 1. |
|
color: color to draw the keypoints with. Default is red. |
|
alpha: transparency value between 0 and 1. (default: 0.4) |
|
|
|
Raises: |
|
ValueError: On incorrect data type for image or masks. |
|
""" |
|
if image.dtype != np.uint8: |
|
raise ValueError('`image` not of type np.uint8') |
|
if mask.dtype != np.uint8: |
|
raise ValueError('`mask` not of type np.uint8') |
|
if np.any(np.logical_and(mask != 1, mask != 0)): |
|
raise ValueError('`mask` elements should be in [0, 1]') |
|
if image.shape[:2] != mask.shape: |
|
raise ValueError('The image has spatial dimensions %s but the mask has ' |
|
'dimensions %s' % (image.shape[:2], mask.shape)) |
|
rgb = ImageColor.getrgb(color) |
|
pil_image = Image.fromarray(image) |
|
|
|
solid_color = np.expand_dims( |
|
np.ones_like(mask), axis=2) * np.reshape(list(rgb), [1, 1, 3]) |
|
pil_solid_color = Image.fromarray(np.uint8(solid_color)).convert('RGBA') |
|
pil_mask = Image.fromarray(np.uint8(255.0*alpha*mask)).convert('L') |
|
pil_image = Image.composite(pil_solid_color, pil_image, pil_mask) |
|
np.copyto(image, np.array(pil_image.convert('RGB'))) |
|
|
|
|
|
def visualize_boxes_and_labels_on_image_array( |
|
image, |
|
boxes, |
|
classes, |
|
scores, |
|
category_index, |
|
instance_masks=None, |
|
instance_boundaries=None, |
|
keypoints=None, |
|
use_normalized_coordinates=False, |
|
max_boxes_to_draw=20, |
|
min_score_thresh=.5, |
|
agnostic_mode=False, |
|
line_thickness=4, |
|
groundtruth_box_visualization_color='black', |
|
skip_scores=False, |
|
skip_labels=False): |
|
"""Overlay labeled boxes on an image with formatted scores and label names. |
|
|
|
This function groups boxes that correspond to the same location |
|
and creates a display string for each detection and overlays these |
|
on the image. Note that this function modifies the image in place, and returns |
|
that same image. |
|
|
|
Args: |
|
image: uint8 numpy array with shape (img_height, img_width, 3) |
|
boxes: a numpy array of shape [N, 4] |
|
classes: a numpy array of shape [N]. Note that class indices are 1-based, |
|
and match the keys in the label map. |
|
scores: a numpy array of shape [N] or None. If scores=None, then |
|
this function assumes that the boxes to be plotted are groundtruth |
|
boxes and plot all boxes as black with no classes or scores. |
|
category_index: a dict containing category dictionaries (each holding |
|
category index `id` and category name `name`) keyed by category indices. |
|
instance_masks: a numpy array of shape [N, image_height, image_width] with |
|
values ranging between 0 and 1, can be None. |
|
instance_boundaries: a numpy array of shape [N, image_height, image_width] |
|
with values ranging between 0 and 1, can be None. |
|
keypoints: a numpy array of shape [N, num_keypoints, 2], can |
|
be None |
|
use_normalized_coordinates: whether boxes is to be interpreted as |
|
normalized coordinates or not. |
|
max_boxes_to_draw: maximum number of boxes to visualize. If None, draw |
|
all boxes. |
|
min_score_thresh: minimum score threshold for a box to be visualized |
|
agnostic_mode: boolean (default: False) controlling whether to evaluate in |
|
class-agnostic mode or not. This mode will display scores but ignore |
|
classes. |
|
line_thickness: integer (default: 4) controlling line width of the boxes. |
|
groundtruth_box_visualization_color: box color for visualizing groundtruth |
|
boxes |
|
skip_scores: whether to skip score when drawing a single detection |
|
skip_labels: whether to skip label when drawing a single detection |
|
|
|
Returns: |
|
uint8 numpy array with shape (img_height, img_width, 3) with overlaid boxes. |
|
""" |
|
|
|
|
|
box_to_display_str_map = collections.defaultdict(list) |
|
box_to_color_map = collections.defaultdict(str) |
|
box_to_instance_masks_map = {} |
|
box_to_instance_boundaries_map = {} |
|
box_to_keypoints_map = collections.defaultdict(list) |
|
if not max_boxes_to_draw: |
|
max_boxes_to_draw = boxes.shape[0] |
|
for i in range(min(max_boxes_to_draw, boxes.shape[0])): |
|
if scores is None or scores[i] > min_score_thresh: |
|
box = tuple(boxes[i].tolist()) |
|
if instance_masks is not None: |
|
box_to_instance_masks_map[box] = instance_masks[i] |
|
if instance_boundaries is not None: |
|
box_to_instance_boundaries_map[box] = instance_boundaries[i] |
|
if keypoints is not None: |
|
box_to_keypoints_map[box].extend(keypoints[i]) |
|
if scores is None: |
|
box_to_color_map[box] = groundtruth_box_visualization_color |
|
else: |
|
display_str = '' |
|
if not skip_labels: |
|
if not agnostic_mode: |
|
if classes[i] in category_index.keys(): |
|
class_name = category_index[classes[i]]['name'] |
|
else: |
|
class_name = 'N/A' |
|
display_str = str(class_name) |
|
if not skip_scores: |
|
if not display_str: |
|
display_str = '{}%'.format(int(100*scores[i])) |
|
else: |
|
display_str = '{}: {}%'.format(display_str, int(100*scores[i])) |
|
box_to_display_str_map[box].append(display_str) |
|
if agnostic_mode: |
|
box_to_color_map[box] = 'DarkOrange' |
|
else: |
|
box_to_color_map[box] = STANDARD_COLORS[ |
|
classes[i] % len(STANDARD_COLORS)] |
|
|
|
|
|
for box, color in box_to_color_map.items(): |
|
ymin, xmin, ymax, xmax = box |
|
if instance_masks is not None: |
|
draw_mask_on_image_array( |
|
image, |
|
box_to_instance_masks_map[box], |
|
color=color |
|
) |
|
if instance_boundaries is not None: |
|
draw_mask_on_image_array( |
|
image, |
|
box_to_instance_boundaries_map[box], |
|
color='red', |
|
alpha=1.0 |
|
) |
|
draw_bounding_box_on_image_array( |
|
image, |
|
ymin, |
|
xmin, |
|
ymax, |
|
xmax, |
|
color=color, |
|
thickness=line_thickness, |
|
display_str_list=box_to_display_str_map[box], |
|
use_normalized_coordinates=use_normalized_coordinates) |
|
if keypoints is not None: |
|
draw_keypoints_on_image_array( |
|
image, |
|
box_to_keypoints_map[box], |
|
color=color, |
|
radius=line_thickness / 2, |
|
use_normalized_coordinates=use_normalized_coordinates) |
|
|
|
return image |
|
|
|
|
|
def add_cdf_image_summary(values, name): |
|
"""Adds a tf.summary.image for a CDF plot of the values. |
|
|
|
Normalizes `values` such that they sum to 1, plots the cumulative distribution |
|
function and creates a tf image summary. |
|
|
|
Args: |
|
values: a 1-D float32 tensor containing the values. |
|
name: name for the image summary. |
|
""" |
|
def cdf_plot(values): |
|
"""Numpy function to plot CDF.""" |
|
normalized_values = values / np.sum(values) |
|
sorted_values = np.sort(normalized_values) |
|
cumulative_values = np.cumsum(sorted_values) |
|
fraction_of_examples = (np.arange(cumulative_values.size, dtype=np.float32) |
|
/ cumulative_values.size) |
|
fig = plt.figure(frameon=False) |
|
ax = fig.add_subplot('111') |
|
ax.plot(fraction_of_examples, cumulative_values) |
|
ax.set_ylabel('cumulative normalized values') |
|
ax.set_xlabel('fraction of examples') |
|
fig.canvas.draw() |
|
width, height = fig.get_size_inches() * fig.get_dpi() |
|
image = np.fromstring(fig.canvas.tostring_rgb(), dtype='uint8').reshape( |
|
1, int(height), int(width), 3) |
|
return image |
|
cdf_plot = tf.py_func(cdf_plot, [values], tf.uint8) |
|
tf.summary.image(name, cdf_plot) |
|
|
|
|
|
def add_hist_image_summary(values, bins, name): |
|
"""Adds a tf.summary.image for a histogram plot of the values. |
|
|
|
Plots the histogram of values and creates a tf image summary. |
|
|
|
Args: |
|
values: a 1-D float32 tensor containing the values. |
|
bins: bin edges which will be directly passed to np.histogram. |
|
name: name for the image summary. |
|
""" |
|
|
|
def hist_plot(values, bins): |
|
"""Numpy function to plot hist.""" |
|
fig = plt.figure(frameon=False) |
|
ax = fig.add_subplot('111') |
|
y, x = np.histogram(values, bins=bins) |
|
ax.plot(x[:-1], y) |
|
ax.set_ylabel('count') |
|
ax.set_xlabel('value') |
|
fig.canvas.draw() |
|
width, height = fig.get_size_inches() * fig.get_dpi() |
|
image = np.fromstring( |
|
fig.canvas.tostring_rgb(), dtype='uint8').reshape( |
|
1, int(height), int(width), 3) |
|
return image |
|
hist_plot = tf.py_func(hist_plot, [values, bins], tf.uint8) |
|
tf.summary.image(name, hist_plot) |
|
|
|
|
|
class EvalMetricOpsVisualization(object): |
|
"""Abstract base class responsible for visualizations during evaluation. |
|
|
|
Currently, summary images are not run during evaluation. One way to produce |
|
evaluation images in Tensorboard is to provide tf.summary.image strings as |
|
`value_ops` in tf.estimator.EstimatorSpec's `eval_metric_ops`. This class is |
|
responsible for accruing images (with overlaid detections and groundtruth) |
|
and returning a dictionary that can be passed to `eval_metric_ops`. |
|
""" |
|
__metaclass__ = abc.ABCMeta |
|
|
|
def __init__(self, |
|
category_index, |
|
max_examples_to_draw=5, |
|
max_boxes_to_draw=20, |
|
min_score_thresh=0.2, |
|
use_normalized_coordinates=True, |
|
summary_name_prefix='evaluation_image'): |
|
"""Creates an EvalMetricOpsVisualization. |
|
|
|
Args: |
|
category_index: A category index (dictionary) produced from a labelmap. |
|
max_examples_to_draw: The maximum number of example summaries to produce. |
|
max_boxes_to_draw: The maximum number of boxes to draw for detections. |
|
min_score_thresh: The minimum score threshold for showing detections. |
|
use_normalized_coordinates: Whether to assume boxes and kepoints are in |
|
normalized coordinates (as opposed to absolute coordiantes). |
|
Default is True. |
|
summary_name_prefix: A string prefix for each image summary. |
|
""" |
|
|
|
self._category_index = category_index |
|
self._max_examples_to_draw = max_examples_to_draw |
|
self._max_boxes_to_draw = max_boxes_to_draw |
|
self._min_score_thresh = min_score_thresh |
|
self._use_normalized_coordinates = use_normalized_coordinates |
|
self._summary_name_prefix = summary_name_prefix |
|
self._images = [] |
|
|
|
def clear(self): |
|
self._images = [] |
|
|
|
def add_images(self, images): |
|
"""Store a list of images, each with shape [1, H, W, C].""" |
|
if len(self._images) >= self._max_examples_to_draw: |
|
return |
|
|
|
|
|
self._images.extend(images) |
|
if len(self._images) > self._max_examples_to_draw: |
|
self._images[self._max_examples_to_draw:] = [] |
|
|
|
def get_estimator_eval_metric_ops(self, eval_dict): |
|
"""Returns metric ops for use in tf.estimator.EstimatorSpec. |
|
|
|
Args: |
|
eval_dict: A dictionary that holds an image, groundtruth, and detections |
|
for a batched example. Note that, we use only the first example for |
|
visualization. See eval_util.result_dict_for_batched_example() for a |
|
convenient method for constructing such a dictionary. The dictionary |
|
contains |
|
fields.InputDataFields.original_image: [batch_size, H, W, 3] image. |
|
fields.InputDataFields.original_image_spatial_shape: [batch_size, 2] |
|
tensor containing the size of the original image. |
|
fields.InputDataFields.true_image_shape: [batch_size, 3] |
|
tensor containing the spatial size of the upadded original image. |
|
fields.InputDataFields.groundtruth_boxes - [batch_size, num_boxes, 4] |
|
float32 tensor with groundtruth boxes in range [0.0, 1.0]. |
|
fields.InputDataFields.groundtruth_classes - [batch_size, num_boxes] |
|
int64 tensor with 1-indexed groundtruth classes. |
|
fields.InputDataFields.groundtruth_instance_masks - (optional) |
|
[batch_size, num_boxes, H, W] int64 tensor with instance masks. |
|
fields.DetectionResultFields.detection_boxes - [batch_size, |
|
max_num_boxes, 4] float32 tensor with detection boxes in range [0.0, |
|
1.0]. |
|
fields.DetectionResultFields.detection_classes - [batch_size, |
|
max_num_boxes] int64 tensor with 1-indexed detection classes. |
|
fields.DetectionResultFields.detection_scores - [batch_size, |
|
max_num_boxes] float32 tensor with detection scores. |
|
fields.DetectionResultFields.detection_masks - (optional) [batch_size, |
|
max_num_boxes, H, W] float32 tensor of binarized masks. |
|
fields.DetectionResultFields.detection_keypoints - (optional) |
|
[batch_size, max_num_boxes, num_keypoints, 2] float32 tensor with |
|
keypoints. |
|
|
|
Returns: |
|
A dictionary of image summary names to tuple of (value_op, update_op). The |
|
`update_op` is the same for all items in the dictionary, and is |
|
responsible for saving a single side-by-side image with detections and |
|
groundtruth. Each `value_op` holds the tf.summary.image string for a given |
|
image. |
|
""" |
|
if self._max_examples_to_draw == 0: |
|
return {} |
|
images = self.images_from_evaluation_dict(eval_dict) |
|
|
|
def get_images(): |
|
"""Returns a list of images, padded to self._max_images_to_draw.""" |
|
images = self._images |
|
while len(images) < self._max_examples_to_draw: |
|
images.append(np.array(0, dtype=np.uint8)) |
|
self.clear() |
|
return images |
|
|
|
def image_summary_or_default_string(summary_name, image): |
|
"""Returns image summaries for non-padded elements.""" |
|
return tf.cond( |
|
tf.equal(tf.size(tf.shape(image)), 4), |
|
lambda: tf.summary.image(summary_name, image), |
|
lambda: tf.constant('')) |
|
|
|
update_op = tf.py_func(self.add_images, [[images[0]]], []) |
|
image_tensors = tf.py_func( |
|
get_images, [], [tf.uint8] * self._max_examples_to_draw) |
|
eval_metric_ops = {} |
|
for i, image in enumerate(image_tensors): |
|
summary_name = self._summary_name_prefix + '/' + str(i) |
|
value_op = image_summary_or_default_string(summary_name, image) |
|
eval_metric_ops[summary_name] = (value_op, update_op) |
|
return eval_metric_ops |
|
|
|
@abc.abstractmethod |
|
def images_from_evaluation_dict(self, eval_dict): |
|
"""Converts evaluation dictionary into a list of image tensors. |
|
|
|
To be overridden by implementations. |
|
|
|
Args: |
|
eval_dict: A dictionary with all the necessary information for producing |
|
visualizations. |
|
|
|
Returns: |
|
A list of [1, H, W, C] uint8 tensors. |
|
""" |
|
raise NotImplementedError |
|
|
|
|
|
class VisualizeSingleFrameDetections(EvalMetricOpsVisualization): |
|
"""Class responsible for single-frame object detection visualizations.""" |
|
|
|
def __init__(self, |
|
category_index, |
|
max_examples_to_draw=5, |
|
max_boxes_to_draw=20, |
|
min_score_thresh=0.2, |
|
use_normalized_coordinates=True, |
|
summary_name_prefix='Detections_Left_Groundtruth_Right'): |
|
super(VisualizeSingleFrameDetections, self).__init__( |
|
category_index=category_index, |
|
max_examples_to_draw=max_examples_to_draw, |
|
max_boxes_to_draw=max_boxes_to_draw, |
|
min_score_thresh=min_score_thresh, |
|
use_normalized_coordinates=use_normalized_coordinates, |
|
summary_name_prefix=summary_name_prefix) |
|
|
|
def images_from_evaluation_dict(self, eval_dict): |
|
return draw_side_by_side_evaluation_image( |
|
eval_dict, self._category_index, self._max_boxes_to_draw, |
|
self._min_score_thresh, self._use_normalized_coordinates) |
|
|