import matplotlib.pyplot as plt import matplotlib.cm as mpl_color_map import copy import numpy as np import torch import torch.nn.functional as F from PIL import Image from modules.guided_backprop import GuidedBackprop import sys,os sys.path.append(os.getcwd()) def apply_colormap_on_image(org_im, activation, colormap_name): """ Apply heatmap on image Args: org_img (PIL img): Original image activation_map (numpy arr): Activation map (grayscale) 0-255 colormap_name (str): Name of the colormap """ # Get colormap color_map = mpl_color_map.get_cmap(colormap_name) no_trans_heatmap = color_map(activation) # Change alpha channel in colormap to make sure original image is displayed heatmap = copy.copy(no_trans_heatmap) heatmap[:, :, 3] = 0.4 heatmap = Image.fromarray((heatmap*255).astype(np.uint8)) no_trans_heatmap = Image.fromarray((no_trans_heatmap*255).astype(np.uint8)) # Apply heatmap on iamge org_im = np.uint8(org_im.detach().to("cpu").numpy()[0][0]*255) org_im = Image.fromarray(org_im) heatmap_on_image = Image.new("RGBA", org_im.size) heatmap_on_image = Image.alpha_composite(heatmap_on_image, org_im.convert('RGBA')) heatmap_on_image = Image.alpha_composite(heatmap_on_image, heatmap) return no_trans_heatmap, heatmap_on_image def save_gradient_images(gradient, file_name): """ Exports the original gradient image Args: gradient (np arr): Numpy array of the gradient with shape (3, 224, 224) file_name (str): Full filename including directory and png """ if not os.path.exists('../results'): os.makedirs('../results') # Normalize gradient = gradient - gradient.min() gradient /= gradient.max() # Save image path_to_file = file_name # print("gradient save shape: ", gradient.shape) save_image(gradient, path_to_file) def format_np_output(np_arr): """ This is a (kind of) bandaid fix to streamline saving procedure. It converts all the outputs to the same format which is 3xWxH with using sucecssive if clauses. Args: im_as_arr (Numpy array): Matrix of shape 1xWxH or WxH or 3xWxH """ # Phase/Case 1: The np arr only has 2 dimensions # Result: Add a dimension at the beginning if len(np_arr.shape) == 2: np_arr = np.expand_dims(np_arr, axis=0) # Phase/Case 2: Np arr has only 1 channel (assuming first dim is channel) # Result: Repeat first channel and convert 1xWxH to 3xWxH if np_arr.shape[0] == 1: np_arr = np.repeat(np_arr, 3, axis=0) # Phase/Case 3: Np arr is of shape 3xWxH # Result: Convert it to WxHx3 in order to make it saveable by PIL if np_arr.shape[0] == 3: np_arr = np_arr.transpose(1, 2, 0) # Phase/Case 4: NP arr is normalized between 0-1 # Result: Multiply with 255 and change type to make it saveable by PIL if np.max(np_arr) <= 1: np_arr = (np_arr*255).astype(np.uint8) return np_arr def save_image(im, path): """ Saves a numpy matrix or PIL image as an image Args: im_as_arr (Numpy array): Matrix of shape DxWxH path (str): Path to the image """ if isinstance(im, (np.ndarray, np.generic)): im = format_np_output(im) im = Image.fromarray(im) im.save(path) def module_output_to_numpy(tensor): return tensor.data.to('cpu').numpy() def convert_to_grayscale(im_as_arr): """ Converts 3d image to grayscale Args: im_as_arr (numpy arr): RGB image with shape (D,W,H) returns: grayscale_im (numpy_arr): Grayscale image with shape (1,W,D) """ grayscale_im = np.sum(np.abs(im_as_arr), axis=0) im_max = np.percentile(grayscale_im, 99) im_min = np.min(grayscale_im) grayscale_im = (np.clip((grayscale_im - im_min) / (im_max - im_min), 0, 1)) grayscale_im = np.expand_dims(grayscale_im, axis=0) return grayscale_im class SaveOutput: def __init__(self, totalFeatMaps): self.layer_outputs = [] self.grad_outputs = [] self.first_grads = [] self.totalFeatMaps = totalFeatMaps self.feature_ext = None ### Used on register_forward_hook ### Output up to totalFeatMaps def append_layer_out(self, module, input, output): self.layer_outputs.append(output[0]) ### Appending with earlier index pertaining to earlier layers ### Used on register_backward_hook ### Output up to totalFeatMaps def append_grad_out(self, module, grad_input, grad_output): self.grad_outputs.append(grad_output[0][0]) ### Appending with last-to-first index pertaining to first-to-last layers ### Used as guided backprop mask def append_first_grads(self, module, grad_in, grad_out): self.first_grads.append(grad_in[0]) def clear(self): self.layer_outputs = [] self.grad_outputs = [] self.first_grads = [] def set_feature_ext(self, feature_ext): self.feature_ext = feature_ext def getGuidedGradImg(self, layerNum, input_img): # print("layer outputs shape: ", self.layer_outputs[0].shape) # print("layer grad_outputs shape: ", self.grad_outputs[0].shape) conv_output_img = module_output_to_numpy(self.layer_outputs[layerNum]) grad_output_img = module_output_to_numpy(self.grad_outputs[len(self.grad_outputs)-layerNum-1]) first_grad_output = self.first_grads[0].data.to('cpu').numpy()[0] print("conv_output_img output shape: ", conv_output_img.shape) print("grad_output_img output shape: ", grad_output_img.shape) print("first_grad_output output shape: ", first_grad_output.shape) print("target min max: {}, {}".format(conv_output_img.min(), conv_output_img.max())) print("guided_gradients min max: {}, {}".format(grad_output_img.min(), grad_output_img.max())) weights = np.mean(grad_output_img, axis=(1, 2)) # Take averages for each gradient print("weights shape: ", weights.shape) print("weights min max1: {}, {}".format(weights.min(), weights.max())) # Create empty numpy array for cam # conv_output_img = np.clip(conv_output_img, 0, conv_output_img.max()) cam = np.ones(conv_output_img.shape[1:], dtype=np.float32) print("cam min max1: {}, {}".format(cam.min(), cam.max())) # Multiply each weight with its conv output and then, sum for i, w in enumerate(weights): cam += w * conv_output_img[i, :, :] # cam = np.maximum(cam, 0) print("cam min max2: {}, {}".format(cam.min(), cam.max())) cam = (cam - np.min(cam)) / (np.max(cam) - np.min(cam)) # Normalize between 0-1 cam = np.uint8(cam * 255) # Scale between 0-255 to visualize cam = np.uint8(Image.fromarray(cam).resize((input_img.shape[3], input_img.shape[2]), Image.ANTIALIAS))/255 # cam_gb = np.multiply(cam, first_grad_output) # grayscale_cam_gb = convert_to_grayscale(cam) return cam def getGuidedGradTimesImg(self, layerNum, input_img): grad_output_img = module_output_to_numpy(self.grad_outputs[len(self.grad_outputs)-layerNum-1]) print("grad_output_img output shape: ", grad_output_img.shape) grad_times_image = grad_output_img[0]*input_img.detach().to("cpu").numpy()[0] return grad_times_image ### target_output -- pass a created output tensor with one hot (1s) already in placed, used for guided gradients (first layer) def output_feature_maps(self, targetDir, input_img): # GBP = GuidedBackprop(self.feature_ext, 'resnet34') # guided_grads = GBP.generate_gradients(input_img, one_hot_output_guided, text_for_pred) # print("guided_grads shape: ", guided_grads.shape) for layerNum in range(self.totalFeatMaps): grad_times_image = self.getGuidedGradTimesImg(layerNum, input_img) # save_gradient_images(cam_gb, targetDir + 'GGrad_Cam_Layer{}.jpg'.format(layerNum)) # save_gradient_images(grayscale_cam_gb, targetDir + 'GGrad_Cam_Gray_Layer{}.jpg'.format(layerNum)) ### Output heatmaps grayscale_vanilla_grads = convert_to_grayscale(grad_times_image) save_gradient_images(grayscale_vanilla_grads, targetDir + 'Vanilla_grad_times_image_gray{}.jpg'.format(layerNum))