|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""Utility functions for the visualizer.""" |
|
from absl import logging |
|
import matplotlib.pyplot as plt |
|
import numpy as np |
|
import PIL |
|
import tensorflow as tf |
|
|
|
from deeplab2.data import coco_constants |
|
|
|
|
|
_COLOR_PERTURBATION = 60 |
|
|
|
|
|
def bit_get(val, idx): |
|
"""Gets the bit value. |
|
|
|
Args: |
|
val: Input value, int or numpy int array. |
|
idx: Which bit of the input val. |
|
|
|
Returns: |
|
The "idx"-th bit of input val. |
|
""" |
|
return (val >> idx) & 1 |
|
|
|
|
|
def create_pascal_label_colormap(): |
|
"""Creates a label colormap used in PASCAL VOC segmentation benchmark. |
|
|
|
Returns: |
|
A colormap for visualizing segmentation results. |
|
""" |
|
colormap = np.zeros((512, 3), dtype=int) |
|
ind = np.arange(512, dtype=int) |
|
|
|
for shift in reversed(list(range(8))): |
|
for channel in range(3): |
|
colormap[:, channel] |= bit_get(ind, channel) << shift |
|
ind >>= 3 |
|
|
|
return colormap |
|
|
|
|
|
def create_rgb_from_instance_map(instance_map): |
|
"""Creates an RGB image from an instance map for visualization. |
|
|
|
To assign a color to each instance, if the maximum value of the instance |
|
labels is smaller than the maximum allowed value of Pascal's colormap, we use |
|
Pascal's colormap. Otherwise, we use random and non-repeated colors. |
|
|
|
Args: |
|
instance_map: Numpy array of shape `[height, width]`, the instance map. |
|
|
|
Returns: |
|
instance_image: Numpy array of shape `[height, width, 3]`, the visualized |
|
RGB instance image. |
|
""" |
|
|
|
if np.max(instance_map) < 512: |
|
colormap = create_pascal_label_colormap() |
|
instance_image = colormap[instance_map] |
|
else: |
|
np.random.seed(0) |
|
|
|
used_colors = [(0, 0, 0)] |
|
instanc_map_shape = instance_map.shape |
|
instance_image = np.zeros([instanc_map_shape[0], instanc_map_shape[1], 3], |
|
np.uint8) |
|
instance_ids = np.unique(instance_map) |
|
for instance_id in instance_ids: |
|
|
|
if instance_id == 0: |
|
continue |
|
r = np.random.randint(0, 256, dtype=np.uint8) |
|
g = np.random.randint(0, 256, dtype=np.uint8) |
|
b = np.random.randint(0, 256, dtype=np.uint8) |
|
while (r, g, b) in used_colors: |
|
r = np.random.randint(0, 256, dtype=np.uint8) |
|
g = np.random.randint(0, 256, dtype=np.uint8) |
|
b = np.random.randint(0, 256, dtype=np.uint8) |
|
instance_image[instance_map == instance_id, :] = (r, g, b) |
|
used_colors.append((r, g, b)) |
|
instance_image[instance_map == 0, :] = (0, 0, 0) |
|
|
|
return instance_image |
|
|
|
|
|
def _generate_color(used_colors): |
|
""""Generates a non-repeated color. |
|
|
|
This function first uses the pascal colormap to generate the color. If more |
|
colors are requested, it randomly generates a non-repeated color. |
|
|
|
Args: |
|
used_colors: A list, where each element is a tuple in the format of |
|
(r, g, b). |
|
|
|
Returns: |
|
A tuple representing a color in the format of (r, g, b). |
|
A list, which is the updated `used_colors` with the returned color tuple |
|
appended to it. |
|
""" |
|
|
|
pascal_colormap = create_pascal_label_colormap() |
|
|
|
if len(used_colors) < len(pascal_colormap): |
|
color = tuple(pascal_colormap[len(used_colors)]) |
|
else: |
|
r = np.random.randint(0, 256, dtype=np.uint8) |
|
g = np.random.randint(0, 256, dtype=np.uint8) |
|
b = np.random.randint(0, 256, dtype=np.uint8) |
|
while (r, g, b) in used_colors: |
|
r = np.random.randint(0, 256, dtype=np.uint8) |
|
g = np.random.randint(0, 256, dtype=np.uint8) |
|
b = np.random.randint(0, 256, dtype=np.uint8) |
|
color = (r, g, b) |
|
used_colors.append(color) |
|
|
|
return color, used_colors |
|
|
|
|
|
def overlay_heatmap_on_image(heatmap, |
|
input_image, |
|
dpi=80.0, |
|
add_color_bar=False): |
|
"""Overlays a heatmap on top of an image. |
|
|
|
Args: |
|
heatmap: A numpy array (float32) of shape `[height, width]`, |
|
which is the heatmap of keypoints. |
|
input_image: A numpy array (float32 or uint8) of shape |
|
`[height, width, 3]`, which is an image and all the pixel values are in |
|
the range of [0.0, 255.0]. |
|
dpi: Float, the dpi of the output image. |
|
add_color_bar: Boolean, whether to add a colorbar to the output image. |
|
|
|
Returns: |
|
A numpy array (uint8) of the same shape as the `input_image`. |
|
""" |
|
|
|
|
|
cmap = plt.cm.Reds |
|
|
|
cmap._init() |
|
|
|
cmap._lut[:, -1] = np.linspace(0, 1.0, 259) |
|
|
|
|
|
image = input_image.astype(np.float32) / 255.0 |
|
image_height, image_width, _ = image.shape |
|
fig, ax = plt.subplots( |
|
1, |
|
1, |
|
facecolor='white', |
|
figsize=(image_width / dpi, image_height / dpi), |
|
dpi=dpi) |
|
grid_y, grid_x = np.mgrid[0:image_height, 0:image_width] |
|
cb = ax.contourf(grid_x, grid_y, heatmap, 10, cmap=cmap) |
|
ax.imshow(image) |
|
ax.grid(False) |
|
plt.axis('off') |
|
if add_color_bar: |
|
plt.colorbar(cb) |
|
fig.subplots_adjust(bottom=0) |
|
fig.subplots_adjust(top=1) |
|
fig.subplots_adjust(right=1) |
|
fig.subplots_adjust(left=0) |
|
|
|
|
|
fig.canvas.draw() |
|
|
|
output_image = np.array(fig.canvas.renderer._renderer)[:, :, :-1] |
|
plt.close() |
|
|
|
return output_image |
|
|
|
|
|
|
|
def make_colorwheel(): |
|
"""Generates a color wheel for optical flow visualization. |
|
|
|
Reference implementation: |
|
https://github.com/tomrunia/OpticalFlow_Visualization |
|
|
|
Returns: |
|
flow_image: A numpy array of output image. |
|
""" |
|
|
|
RY = 15 |
|
YG = 6 |
|
GC = 4 |
|
CB = 11 |
|
BM = 13 |
|
MR = 6 |
|
|
|
ncols = RY + YG + GC + CB + BM + MR |
|
colorwheel = np.zeros((ncols, 3)) |
|
col = 0 |
|
|
|
|
|
colorwheel[0:RY, 0] = 255 |
|
colorwheel[0:RY, 1] = np.floor(255 * np.arange(0, RY) / RY) |
|
col = col + RY |
|
|
|
colorwheel[col:col + YG, 0] = 255 - np.floor(255 * np.arange(0, YG) / YG) |
|
colorwheel[col:col + YG, 1] = 255 |
|
col = col + YG |
|
|
|
colorwheel[col:col + GC, 1] = 255 |
|
colorwheel[col:col + GC, 2] = np.floor(255 * np.arange(0, GC) / GC) |
|
col = col + GC |
|
|
|
colorwheel[col:col+CB, 1] = 255 - np.floor(255*np.arange(CB)/CB) |
|
colorwheel[col:col+CB, 2] = 255 |
|
col = col+CB |
|
|
|
colorwheel[col:col + BM, 2] = 255 |
|
colorwheel[col:col + BM, 0] = np.floor(255 * np.arange(0, BM) / BM) |
|
col = col + BM |
|
|
|
colorwheel[col:col+MR, 2] = 255 - np.floor(255*np.arange(MR)/MR) |
|
colorwheel[col:col+MR, 0] = 255 |
|
return colorwheel |
|
|
|
|
|
|
|
def flow_compute_color(u, v): |
|
"""Computes color for 2D flow field. |
|
|
|
Reference implementation: |
|
https://github.com/tomrunia/OpticalFlow_Visualization |
|
|
|
Args: |
|
u: A numpy array of horizontal flow. |
|
v: A numpy array of vertical flow. |
|
|
|
Returns: |
|
flow_image: A numpy array of output image. |
|
""" |
|
|
|
flow_image = np.zeros((u.shape[0], u.shape[1], 3), np.uint8) |
|
|
|
colorwheel = make_colorwheel() |
|
ncols = colorwheel.shape[0] |
|
|
|
rad = np.sqrt(np.square(u) + np.square(v)) |
|
a = np.arctan2(-v, -u) / np.pi |
|
|
|
fk = (a + 1) / 2 * (ncols - 1) |
|
k0 = np.floor(fk).astype(np.int32) |
|
k1 = k0 + 1 |
|
k1[k1 == ncols] = 0 |
|
f = fk - k0 |
|
|
|
for i in range(colorwheel.shape[1]): |
|
tmp = colorwheel[:, i] |
|
color0 = tmp[k0] / 255.0 |
|
color1 = tmp[k1] / 255.0 |
|
color = (1 - f) * color0 + f * color1 |
|
|
|
idx = (rad <= 1) |
|
color[idx] = 1 - rad[idx] * (1 - color[idx]) |
|
color[~idx] = color[~idx] * 0.75 |
|
|
|
|
|
ch_idx = i |
|
flow_image[:, :, ch_idx] = np.floor(255 * color) |
|
|
|
return flow_image |
|
|
|
|
|
def flow_to_color(flow_uv, clip_flow=None): |
|
"""Applies color to 2D flow field. |
|
|
|
Reference implementation: |
|
https://github.com/tomrunia/OpticalFlow_Visualization |
|
|
|
Args: |
|
flow_uv: A numpy array of flow with shape [Height, Width, 2]. |
|
clip_flow: A float to clip the maximum value for the flow. |
|
|
|
Returns: |
|
flow_image: A numpy array of output image. |
|
|
|
Raises: |
|
ValueError: Input flow does not have dimension equals to 3. |
|
ValueError: Input flow does not have shape [H, W, 2]. |
|
""" |
|
|
|
if flow_uv.ndim != 3: |
|
raise ValueError('Input flow must have three dimensions.') |
|
if flow_uv.shape[2] != 2: |
|
raise ValueError('Input flow must have shape [H, W, 2].') |
|
|
|
if clip_flow is not None: |
|
flow_uv = np.clip(flow_uv, 0, clip_flow) |
|
|
|
u = flow_uv[:, :, 0] |
|
v = flow_uv[:, :, 1] |
|
|
|
rad = np.sqrt(np.square(u) + np.square(v)) |
|
rad_max = np.max(rad) |
|
|
|
epsilon = 1e-5 |
|
u = u / (rad_max + epsilon) |
|
v = v / (rad_max + epsilon) |
|
|
|
return flow_compute_color(u, v) |
|
|
|
|
|
def squeeze_batch_dim_and_convert_to_numpy(input_dict): |
|
for key in input_dict: |
|
input_dict[key] = tf.squeeze(input_dict[key], axis=0).numpy() |
|
return input_dict |
|
|
|
|
|
def create_cityscapes_label_colormap(): |
|
"""Creates a label colormap used in CITYSCAPES segmentation benchmark. |
|
|
|
Returns: |
|
A colormap for visualizing segmentation results. |
|
""" |
|
colormap = np.zeros((256, 3), dtype=np.uint8) |
|
colormap[0] = [128, 64, 128] |
|
colormap[1] = [244, 35, 232] |
|
colormap[2] = [70, 70, 70] |
|
colormap[3] = [102, 102, 156] |
|
colormap[4] = [190, 153, 153] |
|
colormap[5] = [153, 153, 153] |
|
colormap[6] = [250, 170, 30] |
|
colormap[7] = [220, 220, 0] |
|
colormap[8] = [107, 142, 35] |
|
colormap[9] = [152, 251, 152] |
|
colormap[10] = [70, 130, 180] |
|
colormap[11] = [220, 20, 60] |
|
colormap[12] = [255, 0, 0] |
|
colormap[13] = [0, 0, 142] |
|
colormap[14] = [0, 0, 70] |
|
colormap[15] = [0, 60, 100] |
|
colormap[16] = [0, 80, 100] |
|
colormap[17] = [0, 0, 230] |
|
colormap[18] = [119, 11, 32] |
|
return colormap |
|
|
|
|
|
def create_motchallenge_label_colormap(): |
|
"""Creates a label colormap used in MOTChallenge-STEP benchmark. |
|
|
|
Returns: |
|
A colormap for visualizing segmentation results. |
|
""" |
|
colormap = np.zeros((256, 3), dtype=np.uint8) |
|
colormap[0] = [244, 35, 232] |
|
colormap[1] = [70, 70, 70] |
|
colormap[2] = [107, 142, 35] |
|
colormap[3] = [70, 130, 180] |
|
colormap[4] = [220, 20, 60] |
|
colormap[5] = [255, 0, 0] |
|
colormap[6] = [119, 11, 32] |
|
return colormap |
|
|
|
|
|
def create_coco_label_colormap(): |
|
"""Creates a label colormap used in COCO dataset. |
|
|
|
Returns: |
|
A colormap for visualizing segmentation results. |
|
""" |
|
|
|
coco_categories = coco_constants.get_coco_reduced_meta() |
|
colormap = np.zeros((256, 3), dtype=np.uint8) |
|
for category in coco_categories: |
|
colormap[category['id']] = category['color'] |
|
return colormap |
|
|
|
|
|
def label_to_color_image(label, colormap_name='cityscapes'): |
|
"""Adds color defined by the colormap derived from the dataset to the label. |
|
|
|
Args: |
|
label: A 2D array with integer type, storing the segmentation label. |
|
colormap_name: A string specifying the name of the dataset. Used for |
|
choosing the right colormap. Currently supported: 'cityscapes', |
|
'motchallenge'. (Default: 'cityscapes') |
|
|
|
Returns: |
|
result: A 2D array with floating type. The element of the array |
|
is the color indexed by the corresponding element in the input label |
|
to the cityscapes colormap. |
|
|
|
Raises: |
|
ValueError: If label is not of rank 2 or its value is larger than color |
|
map maximum entry. |
|
""" |
|
if label.ndim != 2: |
|
raise ValueError('Expect 2-D input label. Got {}'.format(label.shape)) |
|
|
|
if np.max(label) >= 256: |
|
raise ValueError( |
|
'label value too large: {} >= 256.'.format(np.max(label))) |
|
|
|
if colormap_name == 'cityscapes': |
|
colormap = create_cityscapes_label_colormap() |
|
elif colormap_name == 'motchallenge': |
|
colormap = create_motchallenge_label_colormap() |
|
elif colormap_name == 'coco': |
|
colormap = create_coco_label_colormap() |
|
else: |
|
raise ValueError('Could not find a colormap for dataset %s.' % |
|
colormap_name) |
|
return colormap[label] |
|
|
|
|
|
def save_parsing_result(parsing_result, |
|
label_divisor, |
|
thing_list, |
|
save_dir, |
|
filename, |
|
id_to_colormap=None, |
|
colormap_name='cityscapes'): |
|
"""Saves the parsing results. |
|
|
|
The parsing result encodes both semantic segmentation and instance |
|
segmentation results. In order to visualize the parsing result with only |
|
one png file, we adopt the following procedures, similar to the |
|
`visualization.py` provided in the COCO panoptic segmentation evaluation |
|
codes. |
|
|
|
1. Pixels predicted as `stuff` will take the same semantic color defined |
|
in the colormap. |
|
2. Pixels of a predicted `thing` instance will take similar semantic color |
|
defined in the colormap. For example, `car` class takes blue color in |
|
the colormap. Predicted car instance 1 will then be colored with the |
|
blue color perturbed with a small amount of RGB noise. |
|
|
|
Args: |
|
parsing_result: The numpy array to be saved. The data will be converted |
|
to uint8 and saved as png image. |
|
label_divisor: Integer, encoding the semantic segmentation and instance |
|
segmentation results as value = semantic_label * label_divisor + |
|
instance_label. |
|
thing_list: A list containing the semantic indices of the thing classes. |
|
save_dir: String, the directory to which the results will be saved. |
|
filename: String, the image filename. |
|
id_to_colormap: An optional mapping from track ID to color. |
|
colormap_name: A string specifying the dataset to choose the corresponding |
|
color map. Currently supported: 'cityscapes', 'motchallenge'. (Default: |
|
'cityscapes'). |
|
|
|
Raises: |
|
ValueError: If parsing_result is not of rank 2 or its value in semantic |
|
segmentation result is larger than color map maximum entry. |
|
ValueError: If provided colormap_name is not supported. |
|
|
|
Returns: |
|
If id_to_colormap is passed, the updated id_to_colormap will be returned. |
|
""" |
|
if parsing_result.ndim != 2: |
|
raise ValueError('Expect 2-D parsing result. Got {}'.format( |
|
parsing_result.shape)) |
|
semantic_result = parsing_result // label_divisor |
|
instance_result = parsing_result % label_divisor |
|
colormap_max_value = 256 |
|
if np.max(semantic_result) >= colormap_max_value: |
|
raise ValueError('Predicted semantic value too large: {} >= {}.'.format( |
|
np.max(semantic_result), colormap_max_value)) |
|
height, width = parsing_result.shape |
|
colored_output = np.zeros((height, width, 3), dtype=np.uint8) |
|
if colormap_name == 'cityscapes': |
|
colormap = create_cityscapes_label_colormap() |
|
elif colormap_name == 'motchallenge': |
|
colormap = create_motchallenge_label_colormap() |
|
elif colormap_name == 'coco': |
|
colormap = create_coco_label_colormap() |
|
else: |
|
raise ValueError('Could not find a colormap for dataset %s.' % |
|
colormap_name) |
|
|
|
used_colors = set() |
|
if id_to_colormap is not None: |
|
used_colors = set([tuple(val) for val in id_to_colormap.values()]) |
|
np_state = None |
|
else: |
|
|
|
np_state = np.random.RandomState(0) |
|
|
|
unique_semantic_values = np.unique(semantic_result) |
|
for semantic_value in unique_semantic_values: |
|
semantic_mask = semantic_result == semantic_value |
|
if semantic_value in thing_list: |
|
|
|
|
|
unique_instance_values = np.unique(instance_result[semantic_mask]) |
|
for instance_value in unique_instance_values: |
|
instance_mask = np.logical_and(semantic_mask, |
|
instance_result == instance_value) |
|
if id_to_colormap is not None: |
|
if instance_value in id_to_colormap: |
|
colored_output[instance_mask] = id_to_colormap[instance_value] |
|
continue |
|
random_color = perturb_color( |
|
colormap[semantic_value], |
|
_COLOR_PERTURBATION, |
|
used_colors, |
|
random_state=np_state) |
|
colored_output[instance_mask] = random_color |
|
if id_to_colormap is not None: |
|
id_to_colormap[instance_value] = random_color |
|
else: |
|
|
|
colored_output[semantic_mask] = colormap[semantic_value] |
|
used_colors.add(tuple(colormap[semantic_value])) |
|
|
|
pil_image = PIL.Image.fromarray(colored_output.astype(dtype=np.uint8)) |
|
with tf.io.gfile.GFile('{}/{}.png'.format(save_dir, filename), mode='w') as f: |
|
pil_image.save(f, 'PNG') |
|
if id_to_colormap is not None: |
|
return id_to_colormap |
|
|
|
|
|
def perturb_color(color, |
|
noise, |
|
used_colors=None, |
|
max_trials=50, |
|
random_state=None): |
|
"""Pertrubs the color with some noise. |
|
|
|
If `used_colors` is not None, we will return the color that has |
|
not appeared before in it. |
|
|
|
Args: |
|
color: A numpy array with three elements [R, G, B]. |
|
noise: Integer, specifying the amount of perturbing noise. |
|
used_colors: A set, used to keep track of used colors. |
|
max_trials: An integer, maximum trials to generate random color. |
|
random_state: An optional np.random.RandomState. If passed, will be used to |
|
generate random numbers. |
|
|
|
Returns: |
|
A perturbed color that has not appeared in used_colors. |
|
""" |
|
for _ in range(max_trials): |
|
if random_state is not None: |
|
random_color = color + random_state.randint( |
|
low=-noise, high=noise + 1, size=3) |
|
else: |
|
random_color = color + np.random.randint(low=-noise, |
|
high=noise+1, |
|
size=3) |
|
random_color = np.maximum(0, np.minimum(255, random_color)) |
|
if used_colors is None: |
|
return random_color |
|
elif tuple(random_color) not in used_colors: |
|
used_colors.add(tuple(random_color)) |
|
return random_color |
|
logging.warning('Using duplicate random color.') |
|
return random_color |
|
|
|
|
|
def save_annotation(label, |
|
save_dir, |
|
filename, |
|
add_colormap=True, |
|
normalize_to_unit_values=False, |
|
scale_values=False, |
|
colormap_name='cityscapes'): |
|
"""Saves the given label to image on disk. |
|
|
|
Args: |
|
label: The numpy array to be saved. The data will be converted |
|
to uint8 and saved as png image. |
|
save_dir: String, the directory to which the results will be saved. |
|
filename: String, the image filename. |
|
add_colormap: Boolean, add color map to the label or not. |
|
normalize_to_unit_values: Boolean, normalize the input values to [0, 1]. |
|
scale_values: Boolean, scale the input values to [0, 255] for visualization. |
|
colormap_name: A string specifying the dataset to choose the corresponding |
|
color map. Currently supported: 'cityscapes', 'motchallenge'. (Default: |
|
'cityscapes'). |
|
""" |
|
|
|
if add_colormap: |
|
colored_label = label_to_color_image(label, colormap_name) |
|
else: |
|
colored_label = label |
|
if normalize_to_unit_values: |
|
min_value = np.amin(colored_label) |
|
max_value = np.amax(colored_label) |
|
range_value = max_value - min_value |
|
if range_value != 0: |
|
colored_label = (colored_label - min_value) / range_value |
|
|
|
if scale_values: |
|
colored_label = 255. * colored_label |
|
|
|
pil_image = PIL.Image.fromarray(colored_label.astype(dtype=np.uint8)) |
|
with tf.io.gfile.GFile('%s/%s.png' % (save_dir, filename), mode='w') as f: |
|
pil_image.save(f, 'PNG') |
|
|