Skip to content

Workaround for Annotation Without Napari: Using QuPath with bin2cell #59

@Sergio-ote

Description

@Sergio-ote

Hi!
I wanted to share a piece of code with the bin2cell community. Since I wasn’t able to use Napari on my system, I had to find an alternative method to annotate spatial regions in my images. I have limited experience in image processing, I thought the easiest was to annotate the regions in a different software (QuPath, in my case) to then import those annotations into the AnnData object.

However, this turned out to be more challenging than expected. Initially, I didn’t realize that b2c.scaled_he_image() saves images with a default 150-pixel border, which affected alignment. I also ran into issues related to coordinate origins (specifically, differences in where the (0, 0) point is located).

In any case, just if this could help someone avoid the same problems, here’s the code I used to integrate QuPath GeoJSON annotations into my bin2cell AnnData objects:

import json
import scanpy as sc
import pandas as pd
import geopandas as gpd
from shapely.geometry import shape, mapping, Point
import numpy as np
import matplotlib.pyplot as plt
import bin2cell as b2c  # Assuming this is already installed

# Load AnnData object
cdata = sc.read_h5ad("/path/to/your/adata.h5ad")

# Load the GeoJSON exported from QuPath 
gdf = gpd.read_file("/path/to/your/qupath_geojson.geojson")

# Parse the 'classification' column (some entries are None)
def get_label(x):
    try:
        return json.loads(x)["name"]
    except (TypeError, json.JSONDecodeError):
        return "Unlabeled"

gdf["region"] = gdf["classification"].apply(get_label)

# Coordinates are already in (Y, X) and aligned with top-left origin, with matching mpp to the image used in QuPath
coords = b2c.get_mpp_coords(cdata, mpp=0.5, spatial_key="spatial_cropped_150_buffer")

# Name them accordingly
coords_df = pd.DataFrame(coords, columns=["y", "x"])
coords_df["cell_id"] = cdata.obs_names.values

# Build GeoDataFrame using axis labels
cell_gdf = gpd.GeoDataFrame(
    coords_df,
    geometry=gpd.points_from_xy(coords_df["x"], coords_df["y"]),
    crs=gdf.crs
)

# Spatial join (cells inside annotated regions)
joined = gpd.sjoin(cell_gdf, gdf[["region", "geometry"]], how="left", predicate="within")

# Drop duplicate matches per cell (keep first if overlapping regions)
joined = joined.drop_duplicates(subset="cell_id", keep="first")

# Make sure every cell has a label
region_labels = joined.set_index("cell_id")["region"]
region_labels = region_labels.reindex(cdata.obs_names, fill_value="Unlabeled")

# Store in AnnData
cdata.obs["region_annotation"] = region_labels

# Plot the spatial data with region annotations
sc.pl.spatial(cdata, color="region_annotation", spot_size=20, title="Annotated Regions")

Cheers, and thank you for such a fantastic resource!
Sergio

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions