|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
plt.figure(figsize=(10, 3), dpi=300) |
|
|
|
|
|
|
|
|
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)]) |
|
|
|
|
|
plt.xlim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) |
|
|
plt.ylim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) |
|
|
|
|
|
|
|
|
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):]) |
|
|
|
|
|
plt.xlim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) |
|
|
plt.ylim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) |
|
|
|
|
|
|
|
|
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):]) |
|
|
|
|
|
plt.xlim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) |
|
|
plt.ylim([ad_tar_coor.min() - shift, ad_tar_coor.max() + shift]) |
|
|
|
|
|
|
|
|
if boundary_line: |
|
|
plt.axvline(x=ad_tar_coor[:, 0].min(), color='black') |
|
|
plt.axhline(y=ad_tar_coor[:, 1].min(), color='black') |
|
|
|
|
|
|
|
|
plt.axis('off') |
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
plt.figure(figsize=(10, 8), dpi=150) |
|
|
|
|
|
|
|
|
plt.subplot(1, 3, 1) |
|
|
|
|
|
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)]) |
|
|
|
|
|
plt.imshow(tar_img, origin='lower', alpha=0.3) |
|
|
|
|
|
|
|
|
plt.subplot(1, 3, 2) |
|
|
|
|
|
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):]) |
|
|
|
|
|
plt.imshow(src_img, origin='lower', alpha=0.3) |
|
|
|
|
|
|
|
|
plt.subplot(1, 3, 3) |
|
|
|
|
|
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)]) |
|
|
|
|
|
plt.scatter(homo_coor[:, 0], homo_coor[:, 1], marker='+', s=1, c=pca_hex_comb[len(tar_features.T):]) |
|
|
|
|
|
plt.imshow(aligned_image, origin='lower', alpha=0.3) |
|
|
|
|
|
|
|
|
plt.axis('off') |
|
|
|
|
|
|
|
|
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 not isinstance(color, list): |
|
|
color = [color] * len(polygon) |
|
|
|
|
|
|
|
|
for i, poly in enumerate(polygon): |
|
|
|
|
|
c = color[i] |
|
|
|
|
|
|
|
|
c = color_string_to_rgb(c) |
|
|
|
|
|
|
|
|
t = thickness[i] if isinstance(thickness, list) else thickness |
|
|
|
|
|
|
|
|
poly = np.array(poly, np.int32) |
|
|
|
|
|
|
|
|
poly = poly.reshape((-1, 1, 2)) |
|
|
|
|
|
|
|
|
|
|
|
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`. |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
blended = cv2.addWeighted(image1, alpha, image2, 1 - alpha, 0) |
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
color_string = color_string.replace(' ', '') |
|
|
|
|
|
|
|
|
if color_string.startswith('#'): |
|
|
color_string = color_string[1:] |
|
|
else: |
|
|
|
|
|
|
|
|
if color_string == 'k': |
|
|
color_string = '000000' |
|
|
elif color_string == 'r': |
|
|
color_string = 'ff0000' |
|
|
elif color_string == 'g': |
|
|
color_string = '00ff00' |
|
|
elif color_string == 'b': |
|
|
color_string = '0000ff' |
|
|
elif color_string == 'w': |
|
|
color_string = 'ffffff' |
|
|
else: |
|
|
|
|
|
raise ValueError(f"Unknown color string {color_string}") |
|
|
|
|
|
|
|
|
r = int(color_string[:2], 16) |
|
|
|
|
|
|
|
|
g = int(color_string[2:4], 16) |
|
|
|
|
|
|
|
|
b = int(color_string[4:], 16) |
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
image = cv2.imread(image_path) |
|
|
image_size = (image.shape[0], image.shape[1]) |
|
|
coor = [(x // downsize, y // downsize) for x, y in coor] |
|
|
patch_size = (patch_size[0] // downsize, patch_size[1] // downsize) |
|
|
|
|
|
|
|
|
cmap = plt.get_cmap(cmap) |
|
|
norm = plt.Normalize(vmin=similairty.min(), vmax=similairty.max()) |
|
|
colors = cmap(norm(similairty)) |
|
|
|
|
|
|
|
|
heatmap = np.ones((image_size[0], image_size[1], 3)) * 255 |
|
|
|
|
|
|
|
|
for i in range(len(coor)): |
|
|
x, y = coor[i] |
|
|
w = colors[i][:3] * 255 |
|
|
w = w.astype(np.uint8) |
|
|
heatmap[y:y + patch_size[0], x:x + patch_size[1], :] = w |
|
|
|
|
|
|
|
|
if image_alpha > 0: |
|
|
image = np.array(image) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
if polygons is not None: |
|
|
polygons = [poly // downsize for poly in polygons] |
|
|
image_polygons = draw_polygon(image, polygons, color=polygons_color, thickness=polygons_thickness) |
|
|
heatmap_polygons = draw_polygon(heatmap, polygons, color=polygons_color, thickness=polygons_thickness) |
|
|
|
|
|
return heatmap_polygons, image_polygons |
|
|
else: |
|
|
return heatmap, 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. |
|
|
""" |
|
|
|
|
|
|
|
|
fig, ax = plt.subplots(1, 2, figsize=(15,8)) |
|
|
|
|
|
|
|
|
ax[0].imshow(image1) |
|
|
|
|
|
|
|
|
ax[1].imshow(image2) |
|
|
|
|
|
|
|
|
ax[0].set_title(title1) |
|
|
|
|
|
|
|
|
ax[1].set_title(title2) |
|
|
|
|
|
|
|
|
ax[0].axis('off') |
|
|
ax[1].axis('off') |
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
plt.figure(figsize=(10, 10)) |
|
|
|
|
|
|
|
|
plt.imshow(fullres_img) |
|
|
|
|
|
|
|
|
for polygon in roi_polygon: |
|
|
x, y = zip(*polygon) |
|
|
plt.plot(x, y, color='black', linewidth=linewidth) |
|
|
|
|
|
|
|
|
plt.xlim(xlim) |
|
|
|
|
|
|
|
|
plt.ylim(ylim) |
|
|
|
|
|
|
|
|
plt.gca().invert_yaxis() |
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
plt.figure(figsize=(10, 10)) |
|
|
|
|
|
|
|
|
|
|
|
plt.scatter( |
|
|
st_ad.obsm['spatial'][:, 0], st_ad.obsm['spatial'][:, 1], |
|
|
c=st_ad.obs['bulk_simi'], |
|
|
s=s, |
|
|
vmin=0.1, vmax=0.95, |
|
|
cmap='turbo' |
|
|
) |
|
|
|
|
|
|
|
|
for polygon in roi_polygon: |
|
|
x, y = zip(*polygon) |
|
|
plt.plot(x, y, color='black', linewidth=linewidth) |
|
|
|
|
|
|
|
|
plt.xlim(xlim) |
|
|
|
|
|
|
|
|
plt.ylim(ylim) |
|
|
|
|
|
|
|
|
plt.gca().invert_yaxis() |
|
|
|
|
|
|
|
|
plt.axis('off') |
|
|
|
|
|
|
|
|
|