import matplotlib.pyplot as plt from pathlib import Path import json import cv2 from matplotlib import cm import pandas as pd import numpy as np from tqdm import tqdm def plot_alignment(ad_tar_coor, ad_src_coor, homo_coor, pca_hex_comb, tar_features, shift=300, s=0.8, boundary_line=True): """ Plots the target coordinates and alignment of source coordinates. :param ad_tar_coor: Numpy array of target coordinates to be plotted in the first subplot. :param ad_src_coor: Numpy array of source coordinates to be plotted in the second subplot. :param homo_coor: Numpy array of alignment of source coordinates to be plotted in the third subplot. :param pca_hex_comb: Color values (e.g., PCA or hex values) for plotting the coordinates. :param tar_features: Feature matrix for the target, used to split color values between target and source data. :param shift: Value used to adjust the plot limits around the coordinates for better visualization. Default is 300. :param s: Marker size for the scatter plot points. Default is 0.8. :param boundary_line: Boolean indicating whether to draw boundary lines (horizontal and vertical lines). Default is True. :return: Displays the alignment plot of target, source, and alignment of source coordinates. """ # Create a figure with three subplots, adjusting size and resolution plt.figure(figsize=(10, 3), dpi=300) # First subplot: Plot target coordinates plt.subplot(1, 3, 1) plt.scatter(ad_tar_coor[:, 0], ad_tar_coor[:, 1], marker='o', s=s, c=pca_hex_comb[:len(tar_features.T)]) # Set plot limits based on the minimum and maximum target coordinates, with extra padding from 'shift' plt.xlim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) plt.ylim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) # Second subplot: Plot source coordinates plt.subplot(1, 3, 2) plt.scatter(ad_src_coor[:, 0], ad_src_coor[:, 1], marker='o', s=s, c=pca_hex_comb[len(tar_features.T):]) # Ensure consistent plot limits across subplots by using the same limits as the target coordinates plt.xlim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) plt.ylim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) # Third subplot: Plot alignment of source coordinates plt.subplot(1, 3, 3) plt.scatter(homo_coor[:, 0], homo_coor[:, 1], marker='o', s=s, c=pca_hex_comb[len(tar_features.T):]) # Maintain the same plot limits across all subplots for a uniform comparison plt.xlim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) plt.ylim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) # Optionally draw boundary lines at the minimum x and y values of the target coordinates if boundary_line: plt.axvline(x=ad_tar_coor[:, 0].min(), color='black') # Vertical boundary line at the minimum x of target coordinates plt.axhline(y=ad_tar_coor[:, 1].min(), color='black') # Horizontal boundary line at the minimum y of target coordinates # Remove the axis labels and ticks from all subplots for a cleaner appearance plt.axis('off') # Display the plot plt.show() def plot_alignment_with_img(ad_tar_coor, ad_src_coor, homo_coor, tar_img, src_img, aligned_image, pca_hex_comb, tar_features): """ Plots the target coordinates and alignment of source coordinates with their respective images in the background. :param ad_tar_coor: Numpy array of target coordinates to be plotted in the first and third subplots. :param ad_src_coor: Numpy array of source coordinates to be plotted in the second subplot. :param homo_coor: Numpy array of alignment of source coordinates to be plotted in the third subplot. :param tar_img: Image associated with the target coordinates, used as the background in the first subplot. :param src_img: Image associated with the source coordinates, used as the background in the second subplot. :param aligned_image: Image associated with the aligned coordinates, used as the background in the third subplot. :param pca_hex_comb: Color values (e.g., PCA or hex values) for plotting the coordinates. :param tar_features: Feature matrix for the target, used to split color values between target and source data. :return: Displays the alignment plot of target, source, and alignment of source coordinates with their associated images. """ # Create a figure with three subplots and set the size and resolution plt.figure(figsize=(10, 8), dpi=150) # First subplot: Plot target coordinates with the target image as the background plt.subplot(1, 3, 1) # Scatter plot for the target coordinates with transparency and small marker size plt.scatter(ad_tar_coor[:, 0], ad_tar_coor[:, 1], marker='o', alpha=0.8, s=1, c=pca_hex_comb[:len(tar_features.T)]) # Overlay the target image with some transparency (alpha = 0.3) plt.imshow(tar_img, origin='lower', alpha=0.3) # Second subplot: Plot source coordinates with the source image as the background plt.subplot(1, 3, 2) # Scatter plot for the source coordinates with transparency and small marker size plt.scatter(ad_src_coor[:, 0], ad_src_coor[:, 1], marker='o', alpha=0.8, s=1, c=pca_hex_comb[len(tar_features.T):]) # Overlay the source image with some transparency (alpha = 0.3) plt.imshow(src_img, origin='lower', alpha=0.3) # Third subplot: Plot both target and alignment of source coordinates with the aligned image as the background plt.subplot(1, 3, 3) # Scatter plot for the target coordinates with lower opacity (alpha = 0.2) plt.scatter(ad_tar_coor[:, 0], ad_tar_coor[:, 1], marker='o', alpha=0.2, s=1, c=pca_hex_comb[:len(tar_features.T)]) # Scatter plot for the homologous coordinates with a '+' marker and the same color mapping plt.scatter(homo_coor[:, 0], homo_coor[:, 1], marker='+', s=1, c=pca_hex_comb[len(tar_features.T):]) # Overlay the aligned image with some transparency (alpha = 0.3) plt.imshow(aligned_image, origin='lower', alpha=0.3) # Turn off the axis for all subplots to give a cleaner visual output plt.axis('off') # Display the plots plt.show() def draw_polygon(image, polygon, color='k', thickness=2): """ Draws one or more polygons on the given image. :param image: The image on which to draw the polygons (as a numpy array). :param polygon: A list of polygons, where each polygon is a list of (x, y) coordinate tuples. :param color: A string or list of strings representing the color(s) for each polygon. If a single color is provided, it will be applied to all polygons. Default is 'k' (black). :param thickness: An integer or a list of integers representing the thickness of the polygon borders. If a single value is provided, it will be applied to all polygons. Default is 2. :return: The image with the polygons drawn on it. """ # If the provided `color` is a single value (string), convert it to a list of the same color for each polygon if not isinstance(color, list): color = [color] * len(polygon) # Create a list where each polygon gets the same color # Loop through each polygon in the list, along with its corresponding color for i, poly in enumerate(polygon): # Get the color for the current polygon c = color[i] # Convert the color from a string format (e.g., 'k' or '#ff0000') to an RGB tuple c = color_string_to_rgb(c) # Get the thickness value for the current polygon (if a list is provided, use the corresponding value) t = thickness[i] if isinstance(thickness, list) else thickness # Convert the polygon coordinates to a numpy array of integers poly = np.array(poly, np.int32) # Reshape the polygon array to match OpenCV's expected input format: (number of points, 1, 2) poly = poly.reshape((-1, 1, 2)) # Draw the polygon on the image using OpenCV's `cv2.polylines` function # `isClosed=True` indicates that the polygon should be closed (start and end points are connected) image = cv2.polylines(image, [poly], isClosed=True, color=c, thickness=t) return image def blend_images(image1, image2, alpha=0.5): """ Blends two images together. :param image1: Background image, a numpy array of shape (H, W, 3), where H is height, W is width, and 3 represents the RGB color channels. :param image2: Foreground image, a numpy array of shape (H, W, 3), same dimensions as image1. :param alpha: Blending factor, a float between 0 and 1. The value of alpha determines the weight of image1 in the blend, where 0 means only image2 is shown, and 1 means only image1 is shown. Default is 0.5 (equal blending). :return: A blended image, where each pixel is a weighted combination of the corresponding pixels from image1 and image2. The blending is computed as: `blended = alpha * image1 + (1 - alpha) * image2`. """ # Use cv2.addWeighted to blend the two images. # The first image (image1) is weighted by 'alpha', and the second image (image2) is weighted by '1 - alpha'. blended = cv2.addWeighted(image1, alpha, image2, 1 - alpha, 0) # Return the resulting blended image. return blended def color_string_to_rgb(color_string): """ Converts a color string to an RGB tuple. :param color_string: A string representing the color. This can be in hexadecimal form (e.g., '#ff0000') or a shorthand character for basic colors (e.g., 'k' for black, 'r' for red, etc.). :return: A tuple (r, g, b) representing the RGB values of the color, where each value is an integer between 0 and 255. :raises: ValueError: If the color string is not recognized. """ # Remove any spaces in the color string color_string = color_string.replace(' ', '') # If the string starts with a '#', it's a hexadecimal color, so we remove the '#' if color_string.startswith('#'): color_string = color_string[1:] else: # Handle shorthand single-letter color codes by converting them to hex values # 'k' -> black, 'r' -> red, 'g' -> green, 'b' -> blue, 'w' -> white if color_string == 'k': # Black color_string = '000000' elif color_string == 'r': # Red color_string = 'ff0000' elif color_string == 'g': # Green color_string = '00ff00' elif color_string == 'b': # Blue color_string = '0000ff' elif color_string == 'w': # White color_string = 'ffffff' else: # Raise an error if the color string is not recognized raise ValueError(f"Unknown color string {color_string}") # Convert the first two characters to the red (R) value r = int(color_string[:2], 16) # Convert the next two characters to the green (G) value g = int(color_string[2:4], 16) # Convert the last two characters to the blue (B) value b = int(color_string[4:], 16) # Return the RGB values as a tuple return (r, g, b) def plot_heatmap( coor, similairty, image_path=None, patch_size=(256, 256), save_path=None, downsize=32, cmap='turbo', smooth=False, boxes=None, box_color='k', box_thickness=2, polygons=None, polygons_color='k', polygons_thickness=2, image_alpha=0.5 ): """ Plots a heatmap overlaid on an image based on given coordinates and similairty. :param coor: Array of coordinates (N, 2) where N is the number of patches to place on the heatmap. :param similairty: Array of similairty (N,) corresponding to the coordinates. These similairties are mapped to colors using a colormap. :param image_path: Path to the background image on which the heatmap will be overlaid. If None, a blank white background is used. :param patch_size: Size of each patch in pixels (default is 256x256). :param save_path: Path to save the heatmap image. If None, the heatmap is returned instead of being saved. :param downsize: Factor to downsize the image and patches for faster processing. Default is 32. :param cmap: Colormap to map the similairties to colors. Default is 'turbo'. :param smooth: Boolean to indicate if the heatmap should be smoothed. Not implemented in this version. :param boxes: List of boxes in (x, y, w, h) format. If provided, boxes will be drawn on the heatmap. :param box_color: Color of the boxes. Default is black ('k'). :param box_thickness: Thickness of the box outlines. :param polygons: List of polygons (N, 2) to draw on the heatmap. :param polygons_color: Color of the polygon outlines. Default is black ('k'). :param polygons_thickness: Thickness of the polygon outlines. :param image_alpha: Transparency value (0 to 1) for blending the heatmap with the original image. Default is 0.5. :return: - heatmap: The generated heatmap as a numpy array (RGB). - image: The original image with overlaid polygons if provided. """ # Read the background image (if provided), otherwise a blank image image = cv2.imread(image_path) image_size = (image.shape[0], image.shape[1]) # Get the size of the image coor = [(x // downsize, y // downsize) for x, y in coor] # Downsize the coordinates for faster processing patch_size = (patch_size[0] // downsize, patch_size[1] // downsize) # Downsize the patch size # Convert similairties to colors using the provided colormap cmap = plt.get_cmap(cmap) # Get the colormap object norm = plt.Normalize(vmin=similairty.min(), vmax=similairty.max()) # Normalize similairties to map to color range colors = cmap(norm(similairty)) # Convert the normalized similairties to RGB colors # Initialize a blank white heatmap the size of the image heatmap = np.ones((image_size[0], image_size[1], 3)) * 255 # Start with a white background # Place the colored patches on the heatmap according to the coordinates and patch size for i in range(len(coor)): x, y = coor[i] w = colors[i][:3] * 255 # Get the RGB color for the patch, scaling from [0, 1] to [0, 255] w = w.astype(np.uint8) # Convert the color to uint8 heatmap[y:y + patch_size[0], x:x + patch_size[1], :] = w # Place the patch on the heatmap # If the image_alpha is greater than 0, blend the heatmap with the original image if image_alpha > 0: image = np.array(image) # Pad the image if necessary to match the heatmap size if image.shape[0] < heatmap.shape[0]: pad = heatmap.shape[0] - image.shape[0] image = np.pad(image, ((0, pad), (0, 0), (0, 0)), mode='constant', constant_values=255) if image.shape[1] < heatmap.shape[1]: pad = heatmap.shape[1] - heatmap.shape[1] image = np.pad(image, ((0, 0), (0, pad), (0, 0)), mode='constant', constant_values=255) # Convert the image to BGR (for OpenCV compatibility) and blend with the heatmap image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) image = image.astype(np.uint8) heatmap = heatmap.astype(np.uint8) heatmap = blend_images(heatmap, image, alpha=image_alpha) # Blend the heatmap and the image # If polygons are provided, draw them on the heatmap and image if polygons is not None: polygons = [poly // downsize for poly in polygons] # Downsize the polygon coordinates image_polygons = draw_polygon(image, polygons, color=polygons_color, thickness=polygons_thickness) # Draw polygons on the original image heatmap_polygons = draw_polygon(heatmap, polygons, color=polygons_color, thickness=polygons_thickness) # Draw polygons on the heatmap return heatmap_polygons, image_polygons # Return the heatmap and image with polygons drawn on them else: return heatmap, image # Return the heatmap and image def show_images_side_by_side(image1, image2, title1=None, title2=None): """ Displays two images side by side in a single figure. :param image1: The first image to display (as a numpy array). :param image2: The second image to display (as a numpy array). :param title1: The title for the first image. Default is None (no title). :param title2: The title for the second image. Default is None (no title). :return: Displays the images side by side. """ # Create a figure with 2 subplots (1 row, 2 columns), and set the figure size fig, ax = plt.subplots(1, 2, figsize=(15,8)) # Display the first image on the first subplot ax[0].imshow(image1) # Display the second image on the second subplot ax[1].imshow(image2) # Set the title for the first image (if provided) ax[0].set_title(title1) # Set the title for the second image (if provided) ax[1].set_title(title2) # Remove axis labels and ticks for both images to give a cleaner look ax[0].axis('off') ax[1].axis('off') # Show the final figure with both images displayed side by side plt.show() def plot_img_with_annotation(fullres_img, roi_polygon, linewidth, xlim, ylim): """ Plots image with polygons. :param fullres_img: The full-resolution image to display (as a numpy array). :param roi_polygon: A list of polygons, where each polygon is a list of (x, y) coordinate tuples. :param linewidth: The thickness of the lines used to draw the polygons. :param xlim: A tuple (xmin, xmax) defining the x-axis limits for zooming in on a specific region of the image. :param ylim: A tuple (ymin, ymax) defining the y-axis limits for zooming in on a specific region of the image. :return: Displays the image with ROI polygons overlaid. """ # Create a new figure with a fixed size for displaying the image and annotations plt.figure(figsize=(10, 10)) # Display the full-resolution image plt.imshow(fullres_img) # Loop through each polygon in roi_polygon and plot them on the image for polygon in roi_polygon: x, y = zip(*polygon) # Unzip the list of (x, y) tuples into separate x and y coordinate lists plt.plot(x, y, color='black', linewidth=linewidth) # Plot the polygon using the specified linewidth # Set the x-axis limits based on the provided tuple (xlim) plt.xlim(xlim) # Set the y-axis limits based on the provided tuple (ylim) plt.ylim(ylim) # Invert the y-axis to match the typical image display convention (origin at the top-left) plt.gca().invert_yaxis() # Turn off the axis to give a cleaner image display without ticks or labels plt.axis('off') def plot_annotation_heatmap(st_ad, roi_polygon, s, linewidth, xlim, ylim): """ Plots tissue type annotation heatmap. :param st_ad: AnnData object containing coordinates in `obsm['spatial']` and similarity scores in `obs['bulk_simi']`. :param roi_polygon: A list of polygons, where each polygon is a list of (x, y) coordinate tuples. :param s: The size of the scatter plot markers representing each spatial transcriptomics spot. :param linewidth: The thickness of the lines used to draw the polygons. :param xlim: A tuple (xmin, xmax) defining the x-axis limits for zooming in on a specific region of the image. :param ylim: A tuple (ymin, ymax) defining the y-axis limits for zooming in on a specific region of the image. :return: Displays the heatmap with polygons overlaid. """ # Create a new figure with a fixed size for displaying the heatmap and annotations plt.figure(figsize=(10, 10)) # Scatter plot for the spatial transcriptomics data. # The 'spatial' coordinates are plotted with color intensity based on 'bulk_simi' values. plt.scatter( st_ad.obsm['spatial'][:, 0], st_ad.obsm['spatial'][:, 1], # x and y coordinates c=st_ad.obs['bulk_simi'], # Color values based on 'bulk_simi' s=s, # Size of each marker vmin=0.1, vmax=0.95, # Set the range for the color normalization cmap='turbo' # Use the 'turbo' colormap for the heatmap ) # Loop through each polygon in roi_polygon and plot them on the image for polygon in roi_polygon: x, y = zip(*polygon) # Unzip the list of (x, y) tuples into separate x and y coordinate lists plt.plot(x, y, color='black', linewidth=linewidth) # Plot the polygon using the specified linewidth # Set the x-axis limits based on the provided tuple (xlim) plt.xlim(xlim) # Set the y-axis limits based on the provided tuple (ylim) plt.ylim(ylim) # Invert the y-axis to match the typical image display convention (origin at the top-left) plt.gca().invert_yaxis() # Turn off the axis to give a cleaner image display without ticks or labels plt.axis('off')