-
Notifications
You must be signed in to change notification settings - Fork 28
/
visualization_tools.py
113 lines (105 loc) · 5.62 KB
/
visualization_tools.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# from vis.visualization import visualize_cam
# import matplotlib.cm as cm
from lime import lime_image
from tensorflow.keras import Model
import tensorflow as tf, numpy as np, cv2.cv2 as cv2
# deprecated GradCAM function
# def generate_heatmap(model, input_image):
# grads = visualize_cam(model, layer_idx=-1, filter_indices=None, seed_input=input_image, backprop_modifier=None,
# grad_modifier=None, penultimate_layer_idx=None)
# jet_heatmap = np.uint8(cm.jet(grads)[:, :, :, 0] * 255)
# return jet_heatmap
def generate_explanation(model, input_image):
explainer = lime_image.LimeImageExplainer()
explanation = explainer.explain_instance(image=input_image, classifier_fn=model.predict, hide_color=0,
num_samples=1000, random_seed=18)
temp, mask = explanation.get_image_and_mask(label=explanation.top_labels[0], positive_only=True, num_features=5,
hide_rest=False)
return temp, mask
class GradCAM:
def __init__(self, model, classIdx, layerName=None):
# store the model, the class index used to measure the class
# activation map, and the layer to be used when visualizing
# the class activation map
self.model = model
self.classIdx = classIdx
self.layerName = layerName
# if the layer name is None, attempt to automatically find
# the target output layer
if self.layerName is None:
self.layerName = self.find_target_layer()
def find_target_layer(self):
# attempt to find the final convolutional layer in the network
# by looping over the layers of the network in reverse order
for layer in reversed(self.model.layers):
# check to see if the layer has a 4D output
if len(layer.output_shape) == 4:
return layer.name
# otherwise, we could not find a 4D layer so the GradCAM
# algorithm cannot be applied
raise ValueError("Could not find 4D layer. Cannot apply GradCAM.")
def compute_heatmap(self, image, eps=1e-8, normalize=True):
# construct our gradient model by supplying (1) the inputs
# to our pre-trained model, (2) the output of the (presumably)
# final 4D layer in the network, and (3) the output of the
# softmax activations from the model
gradModel = Model(inputs=[self.model.input], outputs=[self.model.get_layer(self.layerName).output,
self.model.output])
# record operations for automatic differentiation
with tf.GradientTape() as tape:
# cast the image tensor to a float-32 data type, pass the
# image through the gradient model, and grab the loss
# associated with the specific class index
inputs = tf.cast(image, tf.float32)
inputs = np.expand_dims(inputs, axis=0)
(convOutputs, predictions) = gradModel(inputs)
loss = predictions[:, self.classIdx]
# use automatic differentiation to compute the gradients
grads = tape.gradient(loss, convOutputs)
# if gradients are too small (GradCAM is zero everywhere)
# equal to changing the value of 'eps' func arg
grads = grads / (grads.numpy().max() - grads.numpy().min())
# compute the guided gradients
castConvOutputs = tf.cast(convOutputs > 0, "float32")
castGrads = tf.cast(grads > 0, "float32")
guidedGrads = castConvOutputs * castGrads * grads
# the convolution and guided gradients have a batch dimension
# (which we don't need) so let's grab the volume itself and
# discard the batch
convOutputs = convOutputs[0]
guidedGrads = guidedGrads[0]
# compute the average of the gradient values, and using them
# as weights, compute the ponderation of the filters with
# respect to the weights
weights = tf.reduce_mean(guidedGrads, axis=(0, 1))
cam = tf.reduce_sum(tf.multiply(weights, convOutputs), axis=-1)
# grab the spatial dimensions of the input image and resize
# the output class activation map to match the input image
# dimensions
h, w = image.shape[:2]
heatmap = cv2.resize(cam.numpy(), (w, h))
print('avg heatmap value:', np.mean(heatmap))
print('max heatmap value:', np.max(heatmap))
# ignore certain values lower than a threshold to get sharper heatmaps
# heatmap[np.where(heatmap < 1)] = 0
if normalize:
# normalize the heatmap such that all values lie in the range
# [0, 1], scale the resulting values to the range [0, 255],
# and then convert to an unsigned 8-bit integer
numer = heatmap - np.min(heatmap)
denom = (heatmap.max() - heatmap.min()) + eps
heatmap = numer / denom
heatmap = (heatmap * 255)
return heatmap.astype("uint8")
def overlay_heatmap(self, heatmap, image, alpha=0.5, colormap=cv2.COLORMAP_VIRIDIS):
# apply the supplied color map to the heatmap and then
# overlay the heatmap on the input image
heatmap = cv2.applyColorMap(heatmap, colormap)
# if the input image is grey-scale, convert it to 3-dim
if len(image.shape) == 2 or image.shape[-1] != 3:
image = cv2.cvtColor(src=image, code=cv2.COLOR_GRAY2RGB)
# if image px values are in [0, 1], upscale to [0, 255]
if np.max(image) <= 1.0:
image = image * 255.0
output = cv2.addWeighted(image.astype('uint8'), alpha, heatmap, 1 - alpha, 0)
return output