ecopulse / src /visualization.py
acibZ's picture
Deploy EcoPulse
43abac3
Raw
History Blame Contribute Delete
5.59 kB
import matplotlib.pyplot as plt
import numpy as np
import cv2
def plot_greenery_overlay(image_np, mask_classifications, output_path=None):
"""
Plots the original image and an overlay where greenery is highlighted.
"""
plt.figure(figsize=(10, 10))
plt.imshow(image_np)
ax = plt.gca()
ax.set_autoscale_on(False)
# Create an aggregate mask for greenery
h, w = image_np.shape[:2]
greenery_overlay = np.zeros((h, w, 4))
green_color = np.array([0, 1, 0, 0.5]) # Semi-transparent green
for item in mask_classifications:
if item['is_green']:
seg = item['segmentation']
greenery_overlay[seg] = green_color
ax.imshow(greenery_overlay)
plt.axis('off')
if output_path:
plt.savefig(output_path, bbox_inches='tight', pad_inches=0)
plt.close()
else:
plt.show()
def create_greenery_overlay(image_np, mask_classifications):
"""
Creates a semi-transparent greenery overlay composite image.
Returns:
tuple: (composite_rgba, green_masks) where composite_rgba is the
overlay image and green_masks is the list of greenery items.
"""
h, w = image_np.shape[:2]
greenery_overlay = np.zeros((h, w, 4))
green_color = np.array([0, 1, 0, 0.5]) # Semi-transparent green
green_masks = []
for item in mask_classifications:
if item['is_green']:
green_masks.append(item)
seg = item['segmentation']
greenery_overlay[seg] = green_color
base_img_rgba = cv2.cvtColor(image_np, cv2.COLOR_RGB2RGBA)
alpha_mask = greenery_overlay[:, :, 3] > 0
composite = base_img_rgba.copy()
composite[alpha_mask] = (
base_img_rgba[alpha_mask] * 0.5 +
greenery_overlay[alpha_mask] * 255 * 0.5
).astype(np.uint8)
return composite, green_masks
def apply_grad_cam(model, input_tensor, target_class=None):
"""
Generates a Grad-CAM heatmap for the given input tensor using the model's
final convolutional layer (layer4 for ResNet-50).
Args:
model: A GreeneryClassifier instance (wraps ResNet-50 via model.model).
input_tensor: A preprocessed image tensor of shape (1, 3, 64, 64).
target_class: The class index to generate the heatmap for.
If None, uses the model's predicted class.
Returns:
heatmap: A numpy array (H, W) with values in [0, 1] representing
the class activation map.
predicted_class: The class index the model predicted.
"""
import torch
import torch.nn.functional as F
model.eval()
device = next(model.parameters()).device
input_tensor = input_tensor.to(device)
# Storage for hooked values
activations = []
gradients = []
# Hook into the last convolutional block of the inner ResNet-50
target_layer = model.model.layer4
def forward_hook(module, input, output):
activations.append(output.detach())
def backward_hook(module, grad_input, grad_output):
gradients.append(grad_output[0].detach())
fwd_handle = target_layer.register_forward_hook(forward_hook)
bwd_handle = target_layer.register_full_backward_hook(backward_hook)
try:
# Forward pass
output = model(input_tensor)
predicted_class = output.argmax(dim=1).item()
if target_class is None:
target_class = predicted_class
# Backward pass for the target class
model.zero_grad()
class_score = output[0, target_class]
class_score.backward()
# Compute Grad-CAM
act = activations[0] # (1, C, H', W')
grad = gradients[0] # (1, C, H', W')
# Global average pool the gradients to get per-channel weights
weights = grad.mean(dim=[2, 3], keepdim=True) # (1, C, 1, 1)
# Weighted combination of activation maps
cam = (weights * act).sum(dim=1, keepdim=True) # (1, 1, H', W')
cam = F.relu(cam) # Only positive contributions
# Upsample to input size
cam = F.interpolate(cam, size=input_tensor.shape[2:], mode='bilinear', align_corners=False)
cam = cam.squeeze().cpu().numpy()
# Normalize to [0, 1]
if cam.max() > 0:
cam = (cam - cam.min()) / (cam.max() - cam.min())
return cam, predicted_class
finally:
# Guarantee hook cleanup even if an exception fires mid-computation
fwd_handle.remove()
bwd_handle.remove()
def save_grad_cam_overlay(image_np, heatmap, output_path, alpha=0.4):
"""
Overlays a Grad-CAM heatmap on the original image and saves to disk.
Args:
image_np: Original RGB image as numpy array (H, W, 3), values 0-255.
heatmap: Grad-CAM heatmap as numpy array (H, W), values in [0, 1].
output_path: File path to save the result.
alpha: Transparency of the heatmap overlay.
"""
# Resize heatmap to match the image dimensions
heatmap_resized = cv2.resize(heatmap, (image_np.shape[1], image_np.shape[0]))
heatmap_colored = cv2.applyColorMap(np.uint8(255 * heatmap_resized), cv2.COLORMAP_JET)
heatmap_colored = cv2.cvtColor(heatmap_colored, cv2.COLOR_BGR2RGB)
overlay = np.uint8(image_np * (1 - alpha) + heatmap_colored * alpha)
plt.figure(figsize=(10, 10))
plt.imshow(overlay)
plt.axis('off')
plt.title('Grad-CAM Heatmap Overlay')
plt.savefig(output_path, bbox_inches='tight', pad_inches=0)
plt.close()
print(f"Grad-CAM overlay saved to {output_path}")