Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/spatialdata_plot/pl/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ def render_labels(
na_color: ColorLike | None = "default",
outline_alpha: float | int = 0.0,
fill_alpha: float | int = 0.4,
outline_color: ColorLike | tuple[ColorLike] | None = None,
scale: str | None = None,
colorbar: bool | str | None = "auto",
colorbar_params: dict[str, object] | None = None,
Expand Down Expand Up @@ -688,6 +689,11 @@ def render_labels(
Alpha value for the outline of the labels. Invisible by default.
fill_alpha : float | int, default 0.4
Alpha value for the fill of the labels.
outline_color : ColorLike | tuple[ColorLike] | None
Color of the outline of the labels. Can either be a named color ("red"), a hex representation
("#000000") or a list of floats that represent RGB/RGBA values (1.0, 0.0, 0.0, 1.0). If a tuple of colors is
given, the first color is used for the outline and the second color for the fill. If None, the outline color
is set to "black".
scale : str | None
Influences the resolution of the rendering. Possibilities for setting this parameter:
1) None (default). The image is rasterized to fit the canvas size. For multiscale images, the best scale
Expand Down Expand Up @@ -727,6 +733,7 @@ def render_labels(
na_color=na_color,
norm=norm,
outline_alpha=outline_alpha,
outline_color=outline_color,
palette=palette,
scale=scale,
colorbar=colorbar,
Expand All @@ -753,6 +760,7 @@ def render_labels(
cmap_params=cmap_params,
palette=param_values["palette"],
outline_alpha=param_values["outline_alpha"],
outline_color=param_values["outline_color"],
fill_alpha=param_values["fill_alpha"],
transfunc=kwargs.get("transfunc"),
scale=param_values["scale"],
Expand Down
7 changes: 6 additions & 1 deletion src/spatialdata_plot/pl/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -1337,7 +1337,9 @@ def _render_labels(
else:
assert color_source_vector is None

def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float) -> matplotlib.image.AxesImage:
def _draw_labels(
seg_erosionpx: int | None, seg_boundaries: bool, alpha: float, outline_color=None
) -> matplotlib.image.AxesImage:
labels = _map_color_seg(
seg=label.values,
cell_id=instance_id,
Expand All @@ -1347,6 +1349,7 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float)
seg_erosionpx=seg_erosionpx,
seg_boundaries=seg_boundaries,
na_color=render_params.cmap_params.na_color,
outline_color=outline_color,
)

_cax = ax.imshow(
Expand Down Expand Up @@ -1378,6 +1381,7 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float)
seg_erosionpx=render_params.contour_px,
seg_boundaries=True,
alpha=render_params.outline_alpha,
outline_color=render_params.outline_color,
)
alpha_to_decorate_ax = render_params.outline_alpha

Expand All @@ -1391,6 +1395,7 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float)
seg_erosionpx=render_params.contour_px,
seg_boundaries=True,
alpha=render_params.outline_alpha,
outline_color=render_params.outline_color,
)

# pass the less-transparent _cax for the legend
Expand Down
1 change: 1 addition & 0 deletions src/spatialdata_plot/pl/render_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ class LabelsRenderParams:
outline: bool = False
palette: ListedColormap | list[str] | None = None
outline_alpha: float = 1.0
outline_color: ColorLike | tuple[ColorLike] | None = None
fill_alpha: float = 0.4
transfunc: Callable[[float], float] | None = None
scale: str | None = None
Expand Down
39 changes: 34 additions & 5 deletions src/spatialdata_plot/pl/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import dask
import datashader as ds
import matplotlib
import matplotlib.colors as mcolors
import matplotlib.patches as mpatches
import matplotlib.path as mpath
import matplotlib.pyplot as plt
Expand Down Expand Up @@ -54,8 +55,7 @@
from scipy.spatial import ConvexHull
from shapely.errors import GEOSException
from skimage.color import label2rgb
from skimage.morphology import erosion, square
from skimage.segmentation import find_boundaries
from skimage.morphology import erosion, footprint_rectangle
from skimage.util import map_array
from spatialdata import (
SpatialData,
Expand Down Expand Up @@ -1143,6 +1143,7 @@ def _map_color_seg(
na_color: Color,
seg_erosionpx: int | None = None,
seg_boundaries: bool = False,
outline_color: Color | None = None,
) -> ArrayLike:
cell_id = np.array(cell_id)

Expand Down Expand Up @@ -1181,7 +1182,7 @@ def _map_color_seg(
cols = cmap_params.cmap(cmap_params.norm(color_vector))

if seg_erosionpx is not None:
val_im[val_im == erosion(val_im, square(seg_erosionpx))] = 0
val_im[val_im == erosion(val_im, footprint_rectangle((seg_erosionpx, seg_erosionpx)))] = 0

seg_im: ArrayLike = label2rgb(
label=val_im,
Expand All @@ -1194,8 +1195,33 @@ def _map_color_seg(
if seg_boundaries:
if seg.shape[0] == 1:
seg = np.squeeze(seg, axis=0)
seg_bound: ArrayLike = np.clip(seg_im - find_boundaries(seg)[:, :, None], 0, 1)
return np.dstack((seg_bound, np.where(val_im > 0, 1, 0))) # add transparency here

# Binary boundary mask
boundary_mask = seg.astype(bool)

# Ensure seg_im is float in 0-1 and has 3 channels
seg_float = seg_im.astype(float)
if seg_float.ndim == 2:
seg_float = np.stack([seg_float] * 3, axis=-1) # H x W x 3

# Add alpha channel from val_im (preserve original mask)
alpha_channel = (val_im > 0).astype(float)
seg_float = np.dstack((seg_float, alpha_channel)) # H x W x 4

# Convert outline_color to RGBA
if outline_color is None:
outline_rgba = (0, 0, 0, 1.0) # default black
elif isinstance(outline_color, str):
outline_rgba = mcolors.to_rgba(outline_color) # named color or hex string
else:
# assume it's your Color object
outline_rgba = mcolors.to_rgba(outline_color.get_hex_with_alpha())

# Apply outline color to boundary pixels, but keep original alpha from val_im
seg_float[boundary_mask, :3] = outline_rgba[:3] # RGB
seg_float[boundary_mask, 3] = alpha_channel[boundary_mask] * outline_rgba[3] # scale alpha

return seg_float # H x W x 4, valid RGBA

if len(val_im.shape) != len(seg_im.shape):
val_im = np.expand_dims((val_im > 0).astype(int), axis=-1)
Expand Down Expand Up @@ -2413,6 +2439,7 @@ def _validate_label_render_params(
na_color: ColorLike | None,
norm: Normalize | None,
outline_alpha: float | int,
outline_color: ColorLike | tuple[ColorLike] | None,
scale: str | None,
table_name: str | None,
table_layer: str | None,
Expand All @@ -2429,6 +2456,7 @@ def _validate_label_render_params(
"color": color,
"na_color": na_color,
"outline_alpha": outline_alpha,
"outline_color": outline_color,
"cmap": cmap,
"norm": norm,
"scale": scale,
Expand All @@ -2451,6 +2479,7 @@ def _validate_label_render_params(
element_params[el]["fill_alpha"] = param_dict["fill_alpha"]
element_params[el]["scale"] = param_dict["scale"]
element_params[el]["outline_alpha"] = param_dict["outline_alpha"]
element_params[el]["outline_color"] = param_dict["outline_color"]
element_params[el]["contour_px"] = param_dict["contour_px"]
element_params[el]["table_layer"] = param_dict["table_layer"]

Expand Down
Loading