|
|
import os |
|
|
import sys |
|
|
import random |
|
|
import itertools |
|
|
import colorsys |
|
|
|
|
|
import numpy as np |
|
|
from skimage.measure import find_contours |
|
|
import matplotlib.pyplot as plt |
|
|
from matplotlib import patches, lines |
|
|
from matplotlib.patches import Polygon |
|
|
import IPython.display |
|
|
|
|
|
|
|
|
ROOT_DIR = os.path.abspath("../") |
|
|
|
|
|
|
|
|
sys.path.append(ROOT_DIR) |
|
|
from bboxcnn import utils |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def display_images(images, titles=None, cols=4, cmap=None, norm=None, |
|
|
interpolation=None): |
|
|
"""Display the given set of images, optionally with titles. |
|
|
images: list or array of image tensors in HWC format. |
|
|
titles: optional. A list of titles to display with each image. |
|
|
cols: number of images per row |
|
|
cmap: Optional. Color map to use. For example, "Blues". |
|
|
norm: Optional. A Normalize instance to map values to colors. |
|
|
interpolation: Optional. Image interpolation to use for display. |
|
|
""" |
|
|
titles = titles if titles is not None else [""] * len(images) |
|
|
rows = len(images) // cols + 1 |
|
|
plt.figure(figsize=(14, 14 * rows // cols)) |
|
|
i = 1 |
|
|
for image, title in zip(images, titles): |
|
|
plt.subplot(rows, cols, i) |
|
|
plt.title(title, fontsize=9) |
|
|
plt.axis('off') |
|
|
plt.imshow(image.astype(np.uint8), cmap=cmap, |
|
|
norm=norm, interpolation=interpolation) |
|
|
i += 1 |
|
|
plt.show() |
|
|
|
|
|
|
|
|
def random_colors(N, bright=True): |
|
|
""" |
|
|
Generate random colors. |
|
|
To get visually distinct colors, generate them in HSV space then |
|
|
convert to RGB. |
|
|
""" |
|
|
brightness = 1.0 if bright else 0.7 |
|
|
hsv = [(i / N, 1, brightness) for i in range(N)] |
|
|
colors = list(map(lambda c: colorsys.hsv_to_rgb(*c), hsv)) |
|
|
random.shuffle(colors) |
|
|
return colors |
|
|
|
|
|
|
|
|
def apply_mask(image, mask, color, alpha=0.5): |
|
|
"""Apply the given mask to the image. |
|
|
""" |
|
|
for c in range(3): |
|
|
image[:, :, c] = np.where(mask == 1, |
|
|
image[:, :, c] * |
|
|
(1 - alpha) + alpha * color[c] * 255, |
|
|
image[:, :, c]) |
|
|
return image |
|
|
|
|
|
|
|
|
def display_instances(image, boxes, masks, class_ids, class_names, |
|
|
scores=None, title="", |
|
|
figsize=(16, 16), ax=None, |
|
|
show_mask=True, show_bbox=True, |
|
|
colors=None, captions=None): |
|
|
""" |
|
|
boxes: [num_instance, (y1, x1, y2, x2, class_id)] in image coordinates. |
|
|
masks: [height, width, num_instances] |
|
|
class_ids: [num_instances] |
|
|
class_names: list of class names of the dataset |
|
|
scores: (optional) confidence scores for each box |
|
|
title: (optional) Figure title |
|
|
show_mask, show_bbox: To show masks and bounding boxes or not |
|
|
figsize: (optional) the size of the image |
|
|
colors: (optional) An array or colors to use with each object |
|
|
captions: (optional) A list of strings to use as captions for each object |
|
|
""" |
|
|
|
|
|
N = boxes.shape[0] |
|
|
if not N: |
|
|
print("\n*** No instances to display *** \n") |
|
|
else: |
|
|
assert boxes.shape[0] == masks.shape[-1] == class_ids.shape[0] |
|
|
|
|
|
|
|
|
auto_show = False |
|
|
if not ax: |
|
|
_, ax = plt.subplots(1, figsize=figsize) |
|
|
auto_show = True |
|
|
|
|
|
|
|
|
colors = colors or random_colors(N) |
|
|
|
|
|
|
|
|
height, width = image.shape[:2] |
|
|
ax.set_ylim(height + 10, -10) |
|
|
ax.set_xlim(-10, width + 10) |
|
|
ax.axis('off') |
|
|
ax.set_title(title) |
|
|
|
|
|
masked_image = image.astype(np.uint32).copy() |
|
|
for i in range(N): |
|
|
color = colors[i] |
|
|
|
|
|
|
|
|
if not np.any(boxes[i]): |
|
|
|
|
|
continue |
|
|
y1, x1, y2, x2 = boxes[i] |
|
|
if show_bbox: |
|
|
p = patches.Rectangle((x1, y1), x2 - x1, y2 - y1, linewidth=2, |
|
|
alpha=0.7, linestyle="dashed", |
|
|
edgecolor=color, facecolor='none') |
|
|
ax.add_patch(p) |
|
|
|
|
|
|
|
|
if not captions: |
|
|
class_id = class_ids[i] |
|
|
score = scores[i] if scores is not None else None |
|
|
label = class_names[class_id] |
|
|
caption = "{} {:.3f}".format(label, score) if score else label |
|
|
else: |
|
|
caption = captions[i] |
|
|
ax.text(x1, y1 + 8, caption, |
|
|
color='w', size=11, backgroundcolor="none") |
|
|
|
|
|
|
|
|
mask = masks[:, :, i] |
|
|
if show_mask: |
|
|
masked_image = apply_mask(masked_image, mask, color) |
|
|
|
|
|
|
|
|
|
|
|
padded_mask = np.zeros( |
|
|
(mask.shape[0] + 2, mask.shape[1] + 2), dtype=np.uint8) |
|
|
padded_mask[1:-1, 1:-1] = mask |
|
|
contours = find_contours(padded_mask, 0.5) |
|
|
for verts in contours: |
|
|
|
|
|
verts = np.fliplr(verts) - 1 |
|
|
p = Polygon(verts, facecolor="none", edgecolor=color) |
|
|
ax.add_patch(p) |
|
|
ax.imshow(masked_image.astype(np.uint8)) |
|
|
if auto_show: |
|
|
plt.show() |
|
|
|
|
|
|
|
|
def display_differences(image, |
|
|
gt_box, gt_class_id, gt_mask, |
|
|
pred_box, pred_class_id, pred_score, pred_mask, |
|
|
class_names, title="", ax=None, |
|
|
show_mask=True, show_box=True, |
|
|
iou_threshold=0.5, score_threshold=0.5): |
|
|
"""Display ground truth and prediction instances on the same image.""" |
|
|
|
|
|
gt_match, pred_match, overlaps = utils.compute_matches( |
|
|
gt_box, gt_class_id, gt_mask, |
|
|
pred_box, pred_class_id, pred_score, pred_mask, |
|
|
iou_threshold=iou_threshold, score_threshold=score_threshold) |
|
|
|
|
|
colors = [(0, 1, 0, .8)] * len(gt_match)\ |
|
|
+ [(1, 0, 0, 1)] * len(pred_match) |
|
|
|
|
|
class_ids = np.concatenate([gt_class_id, pred_class_id]) |
|
|
scores = np.concatenate([np.zeros([len(gt_match)]), pred_score]) |
|
|
boxes = np.concatenate([gt_box, pred_box]) |
|
|
masks = np.concatenate([gt_mask, pred_mask], axis=-1) |
|
|
|
|
|
captions = ["" for m in gt_match] + ["{:.2f} / {:.2f}".format( |
|
|
pred_score[i], |
|
|
(overlaps[i, int(pred_match[i])] |
|
|
if pred_match[i] > -1 else overlaps[i].max())) |
|
|
for i in range(len(pred_match))] |
|
|
|
|
|
title = title or "Ground Truth and Detections\n GT=green, pred=red, captions: score/IoU" |
|
|
|
|
|
display_instances( |
|
|
image, |
|
|
boxes, masks, class_ids, |
|
|
class_names, scores, ax=ax, |
|
|
show_bbox=show_box, show_mask=show_mask, |
|
|
colors=colors, captions=captions, |
|
|
title=title) |
|
|
|
|
|
|
|
|
def draw_rois(image, rois, refined_rois, mask, class_ids, class_names, limit=10): |
|
|
""" |
|
|
anchors: [n, (y1, x1, y2, x2)] list of anchors in image coordinates. |
|
|
proposals: [n, 4] the same anchors but refined to fit objects better. |
|
|
""" |
|
|
masked_image = image.copy() |
|
|
|
|
|
|
|
|
ids = np.arange(rois.shape[0], dtype=np.int32) |
|
|
ids = np.random.choice( |
|
|
ids, limit, replace=False) if ids.shape[0] > limit else ids |
|
|
|
|
|
fig, ax = plt.subplots(1, figsize=(12, 12)) |
|
|
if rois.shape[0] > limit: |
|
|
plt.title("Showing {} random ROIs out of {}".format( |
|
|
len(ids), rois.shape[0])) |
|
|
else: |
|
|
plt.title("{} ROIs".format(len(ids))) |
|
|
|
|
|
|
|
|
ax.set_ylim(image.shape[0] + 20, -20) |
|
|
ax.set_xlim(-50, image.shape[1] + 20) |
|
|
ax.axis('off') |
|
|
|
|
|
for i, id in enumerate(ids): |
|
|
color = np.random.rand(3) |
|
|
class_id = class_ids[id] |
|
|
|
|
|
y1, x1, y2, x2 = rois[id] |
|
|
p = patches.Rectangle((x1, y1), x2 - x1, y2 - y1, linewidth=2, |
|
|
edgecolor=color if class_id else "gray", |
|
|
facecolor='none', linestyle="dashed") |
|
|
ax.add_patch(p) |
|
|
|
|
|
if class_id: |
|
|
ry1, rx1, ry2, rx2 = refined_rois[id] |
|
|
p = patches.Rectangle((rx1, ry1), rx2 - rx1, ry2 - ry1, linewidth=2, |
|
|
edgecolor=color, facecolor='none') |
|
|
ax.add_patch(p) |
|
|
|
|
|
ax.add_line(lines.Line2D([x1, rx1], [y1, ry1], color=color)) |
|
|
|
|
|
|
|
|
label = class_names[class_id] |
|
|
ax.text(rx1, ry1 + 8, "{}".format(label), |
|
|
color='w', size=11, backgroundcolor="none") |
|
|
|
|
|
|
|
|
m = utils.unmold_mask(mask[id], rois[id] |
|
|
[:4].astype(np.int32), image.shape) |
|
|
masked_image = apply_mask(masked_image, m, color) |
|
|
|
|
|
ax.imshow(masked_image) |
|
|
|
|
|
|
|
|
print("Positive ROIs: ", class_ids[class_ids > 0].shape[0]) |
|
|
print("Negative ROIs: ", class_ids[class_ids == 0].shape[0]) |
|
|
print("Positive Ratio: {:.2f}".format( |
|
|
class_ids[class_ids > 0].shape[0] / class_ids.shape[0])) |
|
|
|
|
|
|
|
|
|
|
|
def draw_box(image, box, color): |
|
|
"""Draw 3-pixel width bounding boxes on the given image array. |
|
|
color: list of 3 int values for RGB. |
|
|
""" |
|
|
y1, x1, y2, x2 = box |
|
|
image[y1:y1 + 2, x1:x2] = color |
|
|
image[y2:y2 + 2, x1:x2] = color |
|
|
image[y1:y2, x1:x1 + 2] = color |
|
|
image[y1:y2, x2:x2 + 2] = color |
|
|
return image |
|
|
|
|
|
|
|
|
def display_top_masks(image, mask, class_ids, class_names, limit=4): |
|
|
"""Display the given image and the top few class masks.""" |
|
|
to_display = [] |
|
|
titles = [] |
|
|
to_display.append(image) |
|
|
titles.append("H x W={}x{}".format(image.shape[0], image.shape[1])) |
|
|
|
|
|
unique_class_ids = np.unique(class_ids) |
|
|
mask_area = [np.sum(mask[:, :, np.where(class_ids == i)[0]]) |
|
|
for i in unique_class_ids] |
|
|
top_ids = [v[0] for v in sorted(zip(unique_class_ids, mask_area), |
|
|
key=lambda r: r[1], reverse=True) if v[1] > 0] |
|
|
|
|
|
for i in range(limit): |
|
|
class_id = top_ids[i] if i < len(top_ids) else -1 |
|
|
|
|
|
m = mask[:, :, np.where(class_ids == class_id)[0]] |
|
|
m = np.sum(m * np.arange(1, m.shape[-1] + 1), -1) |
|
|
to_display.append(m) |
|
|
titles.append(class_names[class_id] if class_id != -1 else "-") |
|
|
display_images(to_display, titles=titles, cols=limit + 1, cmap="Blues_r") |
|
|
|
|
|
|
|
|
def plot_precision_recall(AP, precisions, recalls): |
|
|
"""Draw the precision-recall curve. |
|
|
|
|
|
AP: Average precision at IoU >= 0.5 |
|
|
precisions: list of precision values |
|
|
recalls: list of recall values |
|
|
""" |
|
|
|
|
|
_, ax = plt.subplots(1) |
|
|
ax.set_title("Precision-Recall Curve. AP@50 = {:.3f}".format(AP)) |
|
|
ax.set_ylim(0, 1.1) |
|
|
ax.set_xlim(0, 1.1) |
|
|
_ = ax.plot(recalls, precisions) |
|
|
|
|
|
|
|
|
def plot_overlaps(gt_class_ids, pred_class_ids, pred_scores, |
|
|
overlaps, class_names, threshold=0.5): |
|
|
"""Draw a grid showing how ground truth objects are classified. |
|
|
gt_class_ids: [N] int. Ground truth class IDs |
|
|
pred_class_id: [N] int. Predicted class IDs |
|
|
pred_scores: [N] float. The probability scores of predicted classes |
|
|
overlaps: [pred_boxes, gt_boxes] IoU overlaps of predictions and GT boxes. |
|
|
class_names: list of all class names in the dataset |
|
|
threshold: Float. The prediction probability required to predict a class |
|
|
""" |
|
|
gt_class_ids = gt_class_ids[gt_class_ids != 0] |
|
|
pred_class_ids = pred_class_ids[pred_class_ids != 0] |
|
|
|
|
|
plt.figure(figsize=(12, 10)) |
|
|
plt.imshow(overlaps, interpolation='nearest', cmap=plt.cm.Blues) |
|
|
plt.yticks(np.arange(len(pred_class_ids)), |
|
|
["{} ({:.2f})".format(class_names[int(id)], pred_scores[i]) |
|
|
for i, id in enumerate(pred_class_ids)]) |
|
|
plt.xticks(np.arange(len(gt_class_ids)), |
|
|
[class_names[int(id)] for id in gt_class_ids], rotation=90) |
|
|
|
|
|
thresh = overlaps.max() / 2. |
|
|
for i, j in itertools.product(range(overlaps.shape[0]), |
|
|
range(overlaps.shape[1])): |
|
|
text = "" |
|
|
if overlaps[i, j] > threshold: |
|
|
text = "match" if gt_class_ids[j] == pred_class_ids[i] else "wrong" |
|
|
color = ("white" if overlaps[i, j] > thresh |
|
|
else "black" if overlaps[i, j] > 0 |
|
|
else "grey") |
|
|
plt.text(j, i, "{:.3f}\n{}".format(overlaps[i, j], text), |
|
|
horizontalalignment="center", verticalalignment="center", |
|
|
fontsize=9, color=color) |
|
|
|
|
|
plt.tight_layout() |
|
|
plt.xlabel("Ground Truth") |
|
|
plt.ylabel("Predictions") |
|
|
|
|
|
|
|
|
def draw_boxes(image, boxes=None, refined_boxes=None, |
|
|
masks=None, captions=None, visibilities=None, |
|
|
title="", ax=None): |
|
|
"""Draw bounding boxes and segmentation masks with different |
|
|
customizations. |
|
|
|
|
|
boxes: [N, (y1, x1, y2, x2, class_id)] in image coordinates. |
|
|
refined_boxes: Like boxes, but draw with solid lines to show |
|
|
that they're the result of refining 'boxes'. |
|
|
masks: [N, height, width] |
|
|
captions: List of N titles to display on each box |
|
|
visibilities: (optional) List of values of 0, 1, or 2. Determine how |
|
|
prominent each bounding box should be. |
|
|
title: An optional title to show over the image |
|
|
ax: (optional) Matplotlib axis to draw on. |
|
|
""" |
|
|
|
|
|
assert boxes is not None or refined_boxes is not None |
|
|
N = boxes.shape[0] if boxes is not None else refined_boxes.shape[0] |
|
|
|
|
|
|
|
|
if not ax: |
|
|
_, ax = plt.subplots(1, figsize=(12, 12)) |
|
|
|
|
|
|
|
|
colors = random_colors(N) |
|
|
|
|
|
|
|
|
margin = image.shape[0] // 10 |
|
|
ax.set_ylim(image.shape[0] + margin, -margin) |
|
|
ax.set_xlim(-margin, image.shape[1] + margin) |
|
|
ax.axis('off') |
|
|
|
|
|
ax.set_title(title) |
|
|
|
|
|
masked_image = image.astype(np.uint32).copy() |
|
|
for i in range(N): |
|
|
|
|
|
visibility = visibilities[i] if visibilities is not None else 1 |
|
|
if visibility == 0: |
|
|
color = "gray" |
|
|
style = "dotted" |
|
|
alpha = 0.5 |
|
|
elif visibility == 1: |
|
|
color = colors[i] |
|
|
style = "dotted" |
|
|
alpha = 1 |
|
|
elif visibility == 2: |
|
|
color = colors[i] |
|
|
style = "solid" |
|
|
alpha = 1 |
|
|
|
|
|
|
|
|
if boxes is not None: |
|
|
if not np.any(boxes[i]): |
|
|
|
|
|
continue |
|
|
y1, x1, y2, x2 = boxes[i] |
|
|
p = patches.Rectangle((x1, y1), x2 - x1, y2 - y1, linewidth=2, |
|
|
alpha=alpha, linestyle=style, |
|
|
edgecolor=color, facecolor='none') |
|
|
ax.add_patch(p) |
|
|
|
|
|
|
|
|
if refined_boxes is not None and visibility > 0: |
|
|
ry1, rx1, ry2, rx2 = refined_boxes[i].astype(np.int32) |
|
|
p = patches.Rectangle((rx1, ry1), rx2 - rx1, ry2 - ry1, linewidth=2, |
|
|
edgecolor=color, facecolor='none') |
|
|
ax.add_patch(p) |
|
|
|
|
|
if boxes is not None: |
|
|
ax.add_line(lines.Line2D([x1, rx1], [y1, ry1], color=color)) |
|
|
|
|
|
|
|
|
if captions is not None: |
|
|
caption = captions[i] |
|
|
|
|
|
if refined_boxes is not None: |
|
|
y1, x1, y2, x2 = ry1, rx1, ry2, rx2 |
|
|
ax.text(x1, y1, caption, size=11, verticalalignment='top', |
|
|
color='w', backgroundcolor="none", |
|
|
bbox={'facecolor': color, 'alpha': 0.5, |
|
|
'pad': 2, 'edgecolor': 'none'}) |
|
|
|
|
|
|
|
|
if masks is not None: |
|
|
mask = masks[:, :, i] |
|
|
masked_image = apply_mask(masked_image, mask, color) |
|
|
|
|
|
|
|
|
padded_mask = np.zeros( |
|
|
(mask.shape[0] + 2, mask.shape[1] + 2), dtype=np.uint8) |
|
|
padded_mask[1:-1, 1:-1] = mask |
|
|
contours = find_contours(padded_mask, 0.5) |
|
|
for verts in contours: |
|
|
|
|
|
verts = np.fliplr(verts) - 1 |
|
|
p = Polygon(verts, facecolor="none", edgecolor=color) |
|
|
ax.add_patch(p) |
|
|
ax.imshow(masked_image.astype(np.uint8)) |
|
|
|
|
|
|
|
|
def display_table(table): |
|
|
"""Display values in a table format. |
|
|
table: an iterable of rows, and each row is an iterable of values. |
|
|
""" |
|
|
html = "" |
|
|
for row in table: |
|
|
row_html = "" |
|
|
for col in row: |
|
|
row_html += "<td>{:40}</td>".format(str(col)) |
|
|
html += "<tr>" + row_html + "</tr>" |
|
|
html = "<table>" + html + "</table>" |
|
|
IPython.display.display(IPython.display.HTML(html)) |
|
|
|
|
|
|
|
|
def display_weight_stats(model): |
|
|
"""Scans all the weights in the model and returns a list of tuples |
|
|
that contain stats about each weight. |
|
|
""" |
|
|
layers = model.get_trainable_layers() |
|
|
table = [["WEIGHT NAME", "SHAPE", "MIN", "MAX", "STD"]] |
|
|
for l in layers: |
|
|
weight_values = l.get_weights() |
|
|
weight_tensors = l.weights |
|
|
for i, w in enumerate(weight_values): |
|
|
weight_name = weight_tensors[i].name |
|
|
|
|
|
alert = "" |
|
|
if w.min() == w.max() and not (l.__class__.__name__ == "Conv2D" and i == 1): |
|
|
alert += "<span style='color:red'>*** dead?</span>" |
|
|
if np.abs(w.min()) > 1000 or np.abs(w.max()) > 1000: |
|
|
alert += "<span style='color:red'>*** Overflow?</span>" |
|
|
|
|
|
table.append([ |
|
|
weight_name + alert, |
|
|
str(w.shape), |
|
|
"{:+9.4f}".format(w.min()), |
|
|
"{:+10.4f}".format(w.max()), |
|
|
"{:+9.4f}".format(w.std()), |
|
|
]) |
|
|
display_table(table) |