Skip to content

Commit d7c416c

Browse files
MeyerBendertimtreis
authored andcommitted
implemented outline color for rendering labels
1 parent 4fe68de commit d7c416c

4 files changed

Lines changed: 45 additions & 3 deletions

File tree

src/spatialdata_plot/pl/basic.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,7 @@ def render_labels(
641641
na_color: ColorLike | None = "default",
642642
outline_alpha: float | int = 0.0,
643643
fill_alpha: float | int | None = None,
644+
outline_color: ColorLike | tuple[ColorLike] | None = None,
644645
scale: str | None = None,
645646
colorbar: bool | str | None = "auto",
646647
colorbar_params: dict[str, object] | None = None,
@@ -692,6 +693,10 @@ def render_labels(
692693
fill_alpha : float | int | None, optional
693694
Alpha value for the fill of the labels. By default, it is set to 0.4 or, if a color is given that implies
694695
an alpha, that value is used for `fill_alpha`.
696+
outline_color : ColorLike | None
697+
Color of the outline of the labels. Can either be a named color ("red"), a hex representation
698+
("#000000") or a list of floats that represent RGB/RGBA values (1.0, 0.0, 0.0, 1.0). If None, the outline
699+
color is set to "black".
695700
scale : str | None
696701
Influences the resolution of the rendering. Possibilities for setting this parameter:
697702
1) None (default). The image is rasterized to fit the canvas size. For multiscale images, the best scale
@@ -731,6 +736,7 @@ def render_labels(
731736
na_color=na_color,
732737
norm=norm,
733738
outline_alpha=outline_alpha,
739+
outline_color=outline_color,
734740
palette=palette,
735741
scale=scale,
736742
colorbar=colorbar,
@@ -758,6 +764,7 @@ def render_labels(
758764
cmap_params=cmap_params,
759765
palette=param_values["palette"],
760766
outline_alpha=param_values["outline_alpha"],
767+
outline_color=param_values["outline_color"],
761768
fill_alpha=param_values["fill_alpha"],
762769
transfunc=kwargs.get("transfunc"),
763770
scale=param_values["scale"],

src/spatialdata_plot/pl/render.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1348,7 +1348,8 @@ def _render_labels(
13481348
else:
13491349
assert color_source_vector is None
13501350

1351-
def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float) -> matplotlib.image.AxesImage:
1351+
def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float,
1352+
outline_color: ColorLike | tuple[ColorLike] | None = None) -> matplotlib.image.AxesImage:
13521353
labels = _map_color_seg(
13531354
seg=label.values,
13541355
cell_id=instance_id,
@@ -1358,6 +1359,7 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float)
13581359
seg_erosionpx=seg_erosionpx,
13591360
seg_boundaries=seg_boundaries,
13601361
na_color=na_color,
1362+
outline_color=outline_color,
13611363
)
13621364

13631365
_cax = ax.imshow(
@@ -1389,6 +1391,7 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float)
13891391
seg_erosionpx=render_params.contour_px,
13901392
seg_boundaries=True,
13911393
alpha=render_params.outline_alpha,
1394+
outline_color=render_params.outline_color,
13921395
)
13931396
alpha_to_decorate_ax = render_params.outline_alpha
13941397

@@ -1402,6 +1405,7 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float)
14021405
seg_erosionpx=render_params.contour_px,
14031406
seg_boundaries=True,
14041407
alpha=render_params.outline_alpha,
1408+
outline_color=render_params.outline_color
14051409
)
14061410

14071411
# pass the less-transparent _cax for the legend

src/spatialdata_plot/pl/render_params.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ class LabelsRenderParams:
285285
outline: bool = False
286286
palette: ListedColormap | list[str] | None = None
287287
outline_alpha: float = 1.0
288+
outline_color: ColorLike | tuple[ColorLike] | None = None
288289
fill_alpha: float = 0.4
289290
transfunc: Callable[[float], float] | None = None
290291
scale: str | None = None

src/spatialdata_plot/pl/utils.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import matplotlib.patches as mpatches
1717
import matplotlib.path as mpath
1818
import matplotlib.pyplot as plt
19+
import matplotlib.colors as mcolors
1920
import matplotlib.ticker
2021
import matplotlib.transforms as mtransforms
2122
import numpy as np
@@ -1160,6 +1161,7 @@ def _map_color_seg(
11601161
na_color: Color,
11611162
seg_erosionpx: int | None = None,
11621163
seg_boundaries: bool = False,
1164+
outline_color: Color | None = None,
11631165
) -> ArrayLike:
11641166
cell_id = np.array(cell_id)
11651167

@@ -1211,8 +1213,33 @@ def _map_color_seg(
12111213
if seg_boundaries:
12121214
if seg.shape[0] == 1:
12131215
seg = np.squeeze(seg, axis=0)
1214-
seg_bound: ArrayLike = np.clip(seg_im - find_boundaries(seg)[:, :, None], 0, 1)
1215-
return np.dstack((seg_bound, np.where(val_im > 0, 1, 0))) # add transparency here
1216+
1217+
# Binary boundary mask
1218+
boundary_mask = find_boundaries(seg) # True where boundaries are
1219+
1220+
# Ensure seg_im is float in 0-1 and has 3 channels
1221+
seg_float = seg_im.astype(float)
1222+
if seg_float.ndim == 2:
1223+
seg_float = np.stack([seg_float] * 3, axis=-1) # H x W x 3
1224+
1225+
# Add alpha channel from val_im (preserve original mask)
1226+
alpha_channel = (val_im > 0).astype(float)
1227+
seg_float = np.dstack((seg_float, alpha_channel)) # H x W x 4
1228+
1229+
# Convert outline_color to RGBA
1230+
if outline_color is None:
1231+
outline_rgba = (0, 0, 0, 1.0) # default black
1232+
elif isinstance(outline_color, str):
1233+
outline_rgba = mcolors.to_rgba(outline_color) # named color or hex string
1234+
else:
1235+
# assume it's your Color object
1236+
outline_rgba = mcolors.to_rgba(outline_color.get_hex_with_alpha())
1237+
1238+
# Apply outline color to boundary pixels, but keep original alpha from val_im
1239+
seg_float[boundary_mask, :3] = outline_rgba[:3] # RGB
1240+
seg_float[boundary_mask, 3] = alpha_channel[boundary_mask] * outline_rgba[3] # scale alpha
1241+
1242+
return seg_float # H x W x 4, valid RGBA
12161243

12171244
if len(val_im.shape) != len(seg_im.shape):
12181245
val_im = np.expand_dims((val_im > 0).astype(int), axis=-1)
@@ -2437,6 +2464,7 @@ def _validate_label_render_params(
24372464
na_color: ColorLike | None,
24382465
norm: Normalize | None,
24392466
outline_alpha: float | int,
2467+
outline_color: ColorLike | tuple[ColorLike] | None,
24402468
scale: str | None,
24412469
table_name: str | None,
24422470
table_layer: str | None,
@@ -2453,6 +2481,7 @@ def _validate_label_render_params(
24532481
"color": color,
24542482
"na_color": na_color,
24552483
"outline_alpha": outline_alpha,
2484+
"outline_color": outline_color,
24562485
"cmap": cmap,
24572486
"norm": norm,
24582487
"scale": scale,
@@ -2475,6 +2504,7 @@ def _validate_label_render_params(
24752504
element_params[el]["fill_alpha"] = param_dict["fill_alpha"]
24762505
element_params[el]["scale"] = param_dict["scale"]
24772506
element_params[el]["outline_alpha"] = param_dict["outline_alpha"]
2507+
element_params[el]["outline_color"] = param_dict["outline_color"]
24782508
element_params[el]["contour_px"] = param_dict["contour_px"]
24792509
element_params[el]["table_layer"] = param_dict["table_layer"]
24802510

0 commit comments

Comments
 (0)