# -*- coding: utf-8 -*-
"""GradCAM.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1V6q5-Y0_CxAplqZ17adM33fhXAUCggeF
"""

# Commented out IPython magic to ensure Python compatibility.
import torch                        #Tensor
import torchvision                  #CNN pretrained models  
from torchvision import transforms  #Transforms during test time for input image
import torch.nn.functional as F     #supporting functions
import numpy as np                  #arrays for displaying plots
import cv2                          #Heatmap generation
import matplotlib.pyplot as plt     #displaying plots
import pickle                       #loading ImageNet classe names based on index
# %matplotlib inline
from PIL import Image               #Loading images
from urllib.request import urlopen  #open URL containing ImageNet classes

device = 'cuda' if torch.cuda.is_available() else 'cpu'

"""# GradCAM

# Sub Modules of GradCAM


1.   Extract target layer from our desired model
2.   Hook outputs of forward pass (activations) and backward pass (gradients)
3. Compute weighted activations
4. Upscale weighted activations to input size

One complete process to compute GradCAM:
1.   Clear gradients
2.   Forward pass input
3. Obtain required outputs nodes
4. Backward pass from desired nodes
5. Compute weights of each activation layer
6. Obtain weighted average of target_layer activations
"""

class GradCAM:
  def __init__(self, resnet34, layer_name = 'layer4_basicblock2_conv2'):
    # Save model
    self.resnet34 = resnet34
    # Clear gradients and activations
    self.activations = list()
    self.gradients = list()
    # Extract target layer
    self.target_layer = self.extract_resnet34_layer(resnet34, layer_name)
    # Hook outputs and gradients of target_layer

    #register using forward and backward hooks for the target layer
    self.target_layer.register_forward_hook(self.forward_hook)
    self.target_layer.register_backward_hook(self.backward_hook)

  def forward_hook(self, module, input, output):
      self.activations.append(output)

  def backward_hook(self, module, grad_input, grad_output):
      self.gradients.append(grad_output[0]) #grad_output is a tuple

  def forward(self, input_img):
      #1.clear gradients and hooks
      self.activations = list()
      self.gradients = list()
      self.resnet34.zero_grad()
      #2. forward pass
      out = self.resnet34(input_img)
      #3. Obtain desired output node
      out = out.squeeze()
      out_sort, out_perm = out.sort()
      out_max = out_sort[-1]
      #4. backward pass
      out_max.backward()
      #5. Compute weighted activations
      gradcam = self.compute_weighted_act()
      return gradcam, out_perm[-1] #out_perm[-1]: class of max

  def compute_weighted_act(self):
      """
      Computing grad cam as weighted average of activations of output layer.
      Weights is mean of gradients.
      """
      # Computation of grad mean
      grads = self.gradients[0].squeeze()
      grads = grads.mean(dim=[1,2], keepdim=True)
      # Activations
      acts = self.activations[0]
      acts = acts.squeeze()
      # Weighted activations
      wtd_act = F.relu(grads*acts)
      wtd_act = wtd_act.sum(axis = 0, keepdim = True).unsqueeze(dim=0) #unsqueeze is for upsampling
      wtd_act = wtd_act.detach().cpu()
      wtd_min, wtd_max = wtd_act.min(), wtd_act.max()
      wtd_normalised = (wtd_act - wtd_min)/(wtd_max - wtd_min)
      # Upsample activations to input size
      wtd_upsampled = F.upsample(wtd_normalised, size=(224,224), mode='bicubic')
      return wtd_upsampled
  
  def extract_resnet34_layer(self, model, layer_name):
    """
    currently implementing it for resnet 34
    """
    if 'layer' in layer_name :
      hierarchy = layer_name.split('_')
      index = int(hierarchy[0].lower().lstrip('layer'))
      if index == 1:
        target_layer = model.layer1
      elif index == 2:
        target_layer = model.layer2
      elif index == 3:
        target_layer = model.layer3
      elif index == 4:
        target_layer = model.layer4
      else:
        raise ValueError(f"Unknown layer_name: {layer_name}")

      if 'basicblock' in layer_name:
        index = int(hierarchy[1].lower().lstrip('basicblock'))
        target_layer = target_layer[index] #_modules() is used to extract layers of a sequential block

      if 'conv' in layer_name:
        index = hierarchy[2].lower()
        target_layer = target_layer._modules['conv2']
      return target_layer
  
  def visualise_heatmap(self, input_img, gradcam_out, alpha=1.0):
     gradcam_in = (255 * gradcam_out.squeeze()).type(torch.uint8).cpu().numpy()
     heatmap = cv2.applyColorMap(gradcam_in, cv2.COLORMAP_JET)
     heatmap = torch.from_numpy(heatmap).permute(2,0,1).float().true_divide(255)
     b, g, r = heatmap.split(1)
     heatmap = torch.cat([r, g, b])
     result = heatmap*alpha + input_img.squeeze().to('cpu')
     result = result.detach()
     result = result.true_divide(result.max())
     heatmap = heatmap.detach()
     return heatmap, result


# -*- coding: utf-8 -*-
"""GradCAM.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1V6q5-Y0_CxAplqZ17adM33fhXAUCggeF
"""

# Commented out IPython magic to ensure Python compatibility.
import torch                        #Tensor
import torchvision                  #CNN pretrained models  
from torchvision import transforms  #Transforms during test time for input image
import torch.nn.functional as F     #supporting functions
import numpy as np                  #arrays for displaying plots
import cv2                          #Heatmap generation
import matplotlib.pyplot as plt     #displaying plots
import pickle                       #loading ImageNet classe names based on index
# %matplotlib inline
from PIL import Image               #Loading images
from urllib.request import urlopen  #open URL containing ImageNet classes

device = 'cuda' if torch.cuda.is_available() else 'cpu'

"""# GradCAM

# Sub Modules of GradCAM


1.   Extract target layer from our desired model
2.   Hook outputs of forward pass (activations) and backward pass (gradients)
3. Compute weighted activations
4. Upscale weighted activations to input size

One complete process to compute GradCAM:
1.   Clear gradients
2.   Forward pass input
3. Obtain required outputs nodes
4. Backward pass from desired nodes
5. Compute weights of each activation layer
6. Obtain weighted average of target_layer activations
"""

class GradCAM:
  def __init__(self, resnet34, layer_name = 'layer4_basicblock2_conv2'):
    # Save model
    self.resnet34 = resnet34
    # Clear gradients and activations
    self.activations = list()
    self.gradients = list()
    # Extract target layer
    self.target_layer = self.extract_resnet34_layer(resnet34, layer_name)
    # Hook outputs and gradients of target_layer

    #register using forward and backward hooks for the target layer
    self.target_layer.register_forward_hook(self.forward_hook)
    self.target_layer.register_backward_hook(self.backward_hook)

  def forward_hook(self, module, input, output):
      self.activations.append(output)

  def backward_hook(self, module, grad_input, grad_output):
      self.gradients.append(grad_output[0]) #grad_output is a tuple

  def forward(self, input_img):
      #1.clear gradients and hooks
      self.activations = list()
      self.gradients = list()
      self.resnet34.zero_grad()
      #2. forward pass
      out = self.resnet34(input_img)
      #3. Obtain desired output node
      out = out.squeeze()
      out_sort, out_perm = out.sort()
      out_max = out_sort[-1]
      #4. backward pass
      out_max.backward()
      #5. Compute weighted activations
      gradcam = self.compute_weighted_act()
      return gradcam, out_perm[-1] #out_perm[-1]: class of max

  def compute_weighted_act(self):
      """
      Computing grad cam as weighted average of activations of output layer.
      Weights is mean of gradients.
      """
      # Computation of grad mean
      grads = self.gradients[0].squeeze()
      grads = grads.mean(dim=[1,2], keepdim=True)
      # Activations
      acts = self.activations[0]
      acts = acts.squeeze()
      # Weighted activations
      wtd_act = F.relu(grads*acts)
      wtd_act = wtd_act.sum(axis = 0, keepdim = True).unsqueeze(dim=0) #unsqueeze is for upsampling
      wtd_act = wtd_act.detach().cpu()
      wtd_min, wtd_max = wtd_act.min(), wtd_act.max()
      wtd_normalised = (wtd_act - wtd_min)/(wtd_max - wtd_min)
      # Upsample activations to input size
      wtd_upsampled = F.upsample(wtd_normalised, size=(224,224), mode='bicubic')
      return wtd_upsampled
  
  def extract_resnet34_layer(self, model, layer_name):
    """
    currently implementing it for resnet 34
    """
    if 'layer' in layer_name :
      hierarchy = layer_name.split('_')
      index = int(hierarchy[0].lower().lstrip('layer'))
      if index == 1:
        target_layer = model.layer1
      elif index == 2:
        target_layer = model.layer2
      elif index == 3:
        target_layer = model.layer3
      elif index == 4:
        target_layer = model.layer4
      else:
        raise ValueError(f"Unknown layer_name: {layer_name}")

      if 'basicblock' in layer_name:
        index = int(hierarchy[1].lower().lstrip('basicblock'))
        target_layer = target_layer[index] #_modules() is used to extract layers of a sequential block

      if 'conv' in layer_name:
        index = hierarchy[2].lower()
        target_layer = target_layer._modules['conv2']
      return target_layer
  
  def visualise_heatmap(self, input_img, gradcam_out, alpha=1.0):
     gradcam_in = (255 * gradcam_out.squeeze()).type(torch.uint8).cpu().numpy()
     heatmap = cv2.applyColorMap(gradcam_in, cv2.COLORMAP_JET)
     heatmap = torch.from_numpy(heatmap).permute(2,0,1).float().true_divide(255)
     b, g, r = heatmap.split(1)
     heatmap = torch.cat([r, g, b])
     result = heatmap*alpha + input_img.squeeze().to('cpu')
     result = result.detach()
     result = result.true_divide(result.max())
     heatmap = heatmap.detach()
     return heatmap, result


# -*- coding: utf-8 -*- GradCAM.ipynb  Automatically generated by Colaboratory.  Original file is located at     https://colab.research.google.com/drive/1V6q5-Y0_CxAplqZ17adM33fhXAUCggeF # Commented out IPython magic to ensure Python compatibility. #Tensor #CNN pretrained models #Transforms during test time for input image #supporting functions #arrays for displaying plots #Heatmap generation #displaying plots #loading ImageNet classe names based on index # %matplotlib inline #Loading images #open URL containing ImageNet classes # GradCAM  # Sub Modules of GradCAM   1.   Extract target layer from our desired model 2.   Hook outputs of forward pass (activations) and backward pass (gradients) 3. Compute weighted activations 4. Upscale weighted activations to input size  One complete process to compute GradCAM: 1.   Clear gradients 2.   Forward pass input 3. Obtain required outputs nodes 4. Backward pass from desired nodes 5. Compute weights of each activation layer 6. Obtain weighted average of target_layer activations # Save model # Clear gradients and activations # Extract target layer # Hook outputs and gradients of target_layer #register using forward and backward hooks for the target layer #grad_output is a tuple #1.clear gradients and hooks #2. forward pass #3. Obtain desired output node #4. backward pass #5. Compute weighted activations #out_perm[-1]: class of max Computing grad cam as weighted average of activations of output layer.       Weights is mean of gradients. # Computation of grad mean # Activations # Weighted activations #unsqueeze is for upsampling # Upsample activations to input size currently implementing it for resnet 34 #_modules() is used to extract layers of a sequential block