-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
Copy pathcam.js
129 lines (114 loc) · 5.32 KB
/
cam.js
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
* @license
* Copyright 2019 Google LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =============================================================================
*/
/**
* This script contains a function that performs the following operations:
*
* Get visual interpretation of which parts of the image more most
* responsible for a convnet's classification decision, using the
* gradient-based class activation map (CAM) method.
* See function `gradClassActivationMap()`.
*/
const tf = require('@tensorflow/tfjs');
const utils = require('./utils');
/**
* Calculate class activation map (CAM) and overlay it on input image.
*
* This function automatically finds the last convolutional layer, get its
* output (activation) under the input image, weights its filters by the
* gradient of the class output with respect to them, and then collapses along
* the filter dimension.
*
* @param {tf.Sequential} model A TensorFlow.js sequential model, assumed to
* contain at least one convolutional layer.
* @param {number} classIndex Index to class in the model's final classification
* output.
* @param {tf.Tensor4d} x Input image, assumed to have shape
* `[1, height, width, 3]`.
* @param {number} overlayFactor Optional overlay factor.
* @returns The input image with a heat-map representation of the class
* activation map overlaid on top of it, as float32-type `tf.Tensor4d` of
* shape `[1, height, width, 3]`.
*/
function gradClassActivationMap(model, classIndex, x, overlayFactor = 2.0) {
// Try to locate the last conv layer of the model.
let layerIndex = model.layers.length - 1;
while (layerIndex >= 0) {
if (model.layers[layerIndex].getClassName().startsWith('Conv')) {
break;
}
layerIndex--;
}
tf.util.assert(
layerIndex >= 0, `Failed to find a convolutional layer in model`);
const lastConvLayer = model.layers[layerIndex];
console.log(
`Located last convolutional layer of the model at ` +
`index ${layerIndex}: layer type = ${lastConvLayer.getClassName()}; ` +
`layer name = ${lastConvLayer.name}`);
// Get "sub-model 1", which goes from the original input to the output
// of the last convolutional layer.
const lastConvLayerOutput = lastConvLayer.output;
const subModel1 =
tf.model({inputs: model.inputs, outputs: lastConvLayerOutput});
// Get "sub-model 2", which goes from the output of the last convolutional
// layer to the original output.
const newInput = tf.input({shape: lastConvLayerOutput.shape.slice(1)});
layerIndex++;
let y = newInput;
while (layerIndex < model.layers.length) {
y = model.layers[layerIndex++].apply(y);
}
const subModel2 = tf.model({inputs: newInput, outputs: y});
return tf.tidy(() => {
// This function runs sub-model 2 and extracts the slice of the probability
// output that corresponds to the desired class.
const convOutput2ClassOutput = (input) =>
subModel2.apply(input, {training: true}).gather([classIndex], 1);
// This is the gradient function of the output corresponding to the desired
// class with respect to its input (i.e., the output of the last
// convolutional layer of the original model).
const gradFunction = tf.grad(convOutput2ClassOutput);
// Calculate the values of the last conv layer's output.
const lastConvLayerOutputValues = subModel1.apply(x);
// Calculate the values of gradients of the class output w.r.t. the output
// of the last convolutional layer.
const gradValues = gradFunction(lastConvLayerOutputValues);
// Pool the gradient values within each filter of the last convolutional
// layer, resulting in a tensor of shape [numFilters].
const pooledGradValues = tf.mean(gradValues, [0, 1, 2]);
// Scale the convlutional layer's output by the pooled gradients, using
// broadcasting.
const scaledConvOutputValues =
lastConvLayerOutputValues.mul(pooledGradValues);
// Create heat map by averaging and collapsing over all filters.
let heatMap = scaledConvOutputValues.mean(-1);
// Discard negative values from the heat map and normalize it to the [0, 1]
// interval.
heatMap = heatMap.relu();
heatMap = heatMap.div(heatMap.max()).expandDims(-1);
// Up-sample the heat map to the size of the input image.
heatMap = tf.image.resizeBilinear(heatMap, [x.shape[1], x.shape[2]]);
// Apply an RGB colormap on the heatMap. This step is necessary because
// the heatMap is a 1-channel (grayscale) image. It needs to be converted
// into a color (RGB) one through this function call.
heatMap = utils.applyColorMap(heatMap);
// To form the final output, overlay the color heat map on the input image.
heatMap = heatMap.mul(overlayFactor).add(x.div(255));
return heatMap.div(heatMap.max()).mul(255);
});
}
module.exports = {gradClassActivationMap};