Skip to content

Commit c10b177

Browse files
Sonja-StockhausSonja Stockhauspre-commit-ci[bot]
authored
MultiscaleImage handling (#164)
* first draft, including rasterization in show * working version for images * user can select scale * lower scales are plotted in original size * add _multiscale_to_spatial_image to heuristically select best scale * add _rasterize_if_necessary function and including it into show. Not working yet! * use new get_extent from spatialdata * add scaling logic, not final * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix problem of figure = None when user gives Axes object * commit before merging with main * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * cleanup after merging with main * minor changes * attempt test error fix * remove unnecessary code, add tests, update changelog * attempt test error fix * attempt test error fix 2 * add option for scale=full * fix error when plotting image from only one cs * add tests for rasterization and scale=full case * reintroduce removal of cs w/o relevant elements * update reference images * attempt to resolve pandas version issue * remove pandas constraint * bugfix multiscale image extent, add info logging about selected scale * fix cs bug * update rasterization conditions * match list of scales to elements * dpi & figsize documentation update * remove logging.info --------- Co-authored-by: Sonja Stockhaus <stockhaus@cip.ifi.lmu.de> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 3e09dd1 commit c10b177

15 files changed

Lines changed: 476 additions & 85 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning][].
1212

1313
### Added
1414

15+
- Pushed `get_extent` functionality upstream to `spatialdata` (#162)
16+
- Multiscale image handling: user can specify a scale, else the best scale is selected automatically given the figure size and dpi (#164)
17+
- Large images are automatically rasterized to speed up performance (#164)
18+
1519
### Fixed
1620

1721
## [0.0.6] - 2023-11-06

src/spatialdata_plot/pl/basic.py

Lines changed: 146 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
)
4242
from spatialdata_plot.pl.utils import (
4343
_get_cs_contents,
44+
_get_valid_cs,
4445
_maybe_set_colors,
4546
_mpl_ax_contains_elements,
4647
_prepare_cmap_norm,
@@ -318,6 +319,7 @@ def render_images(
318319
palette: str | list[str] | None = None,
319320
alpha: float = 1.0,
320321
quantiles_for_norm: tuple[float | None, float | None] = (None, None),
322+
scale: str | list[str] | None = None,
321323
**kwargs: Any,
322324
) -> sd.SpatialData:
323325
"""
@@ -340,6 +342,15 @@ def render_images(
340342
Alpha value for the shapes.
341343
quantiles_for_norm
342344
Tuple of (pmin, pmax) which will be used for quantile normalization.
345+
scale
346+
Influences the resolution of the rendering. Possibilities for setting this parameter:
347+
1) None (default). The image is rasterized to fit the canvas size. For multiscale images, the best scale
348+
is selected before the rasterization step.
349+
2) Name of one of the scales in the multiscale image to be rendered. This scale is rendered as it is
350+
(exception: a dpi is specified in `show()`. Then the image is rasterized to fit the canvas and dpi).
351+
3) "full": render the full image without rasterization. In the case of a multiscale image, the scale
352+
with the highest resolution is selected. This can lead to long computing times for large images!
353+
4) List that is matched to the list of elements (can contain `None`, scale names or "full").
343354
kwargs
344355
Additional arguments to be passed to cmap and norm.
345356
@@ -383,6 +394,7 @@ def render_images(
383394
palette=palette,
384395
alpha=alpha,
385396
quantiles_for_norm=quantiles_for_norm,
397+
scale=scale,
386398
)
387399

388400
return sdata
@@ -401,6 +413,7 @@ def render_labels(
401413
na_color: str | tuple[float, ...] | None = (0.0, 0.0, 0.0, 0.0),
402414
outline_alpha: float = 1.0,
403415
fill_alpha: float = 0.3,
416+
scale: str | list[str] | None = None,
404417
**kwargs: Any,
405418
) -> sd.SpatialData:
406419
"""
@@ -433,6 +446,15 @@ def render_labels(
433446
Color to be used for NAs values, if present.
434447
alpha
435448
Alpha value for the labels.
449+
scale
450+
Influences the resolution of the rendering. Possibilities for setting this parameter:
451+
1) None (default). The image is rasterized to fit the canvas size. For multiscale images, the best scale
452+
is selected before the rasterization step.
453+
2) Name of one of the scales in the multiscale image to be rendered. This scale is rendered as it is
454+
(exception: a dpi is specified in `show()`. Then the image is rasterized to fit the canvas and dpi).
455+
3) "full": render the full image without rasterization. In the case of a multiscale image, the scale
456+
with the highest resolution is selected. This can lead to long computing times for large images!
457+
4) List that is matched to the list of elements (can contain `None`, scale names or "full").
436458
kwargs
437459
Additional arguments to be passed to cmap and norm.
438460
@@ -470,6 +492,7 @@ def render_labels(
470492
outline_alpha=outline_alpha,
471493
fill_alpha=fill_alpha,
472494
transfunc=kwargs.get("transfunc", None),
495+
scale=scale,
473496
)
474497

475498
return sdata
@@ -502,15 +525,22 @@ def show(
502525
503526
Parameters
504527
----------
528+
coordinate_systems :
529+
Name(s) of the coordinate system(s) to be plotted. If None, all coordinate systems are plotted.
530+
If a coordinate system doesn't contain any relevant elements (as specified in the render_* calls),
531+
it is automatically not plotted.
532+
figsize :
533+
Size of the figure (width, height) in inches. The size of the actual canvas may deviate from this,
534+
depending on the dpi! In matplotlib, the actual figure size (in pixels) is dpi * figsize.
535+
If None, the default of matlotlib is used (6.4, 4.8)
536+
dpi :
537+
Resolution of the plot in dots per inch (as in matplotlib).
538+
If None, the default of matplotlib is used (100.0).
505539
ax :
506540
Matplotlib axes object to plot on. If None, a new figure is created.
507541
Works only if there is one image in the SpatialData object.
508542
ncols :
509543
Number of columns in the figure. Default is 4.
510-
width :
511-
Width of each subplot. Default is 4.
512-
height :
513-
Height of each subplot. Default is 3.
514544
515545
Returns
516546
-------
@@ -576,6 +606,42 @@ def show(
576606
if cs not in sdata.coordinate_systems:
577607
raise ValueError(f"Unknown coordinate system '{cs}', valid choices are: {sdata.coordinate_systems}")
578608

609+
# Check if user specified only certain elements to be plotted
610+
cs_contents = _get_cs_contents(sdata)
611+
elements_to_be_rendered = []
612+
for cmd, params in render_cmds.items():
613+
if cmd == "render_images" and cs_contents.query(f"cs == '{cs}'")["has_images"][0]: # noqa: SIM114
614+
if params.elements is not None:
615+
elements_to_be_rendered += (
616+
[params.elements] if isinstance(params.elements, str) else params.elements
617+
)
618+
elif cmd == "render_shapes" and cs_contents.query(f"cs == '{cs}'")["has_shapes"][0]: # noqa: SIM114
619+
if params.elements is not None:
620+
elements_to_be_rendered += (
621+
[params.elements] if isinstance(params.elements, str) else params.elements
622+
)
623+
elif cmd == "render_points" and cs_contents.query(f"cs == '{cs}'")["has_points"][0]: # noqa: SIM114
624+
if params.elements is not None:
625+
elements_to_be_rendered += (
626+
[params.elements] if isinstance(params.elements, str) else params.elements
627+
)
628+
elif cmd == "render_labels" and cs_contents.query(f"cs == '{cs}'")["has_labels"][0]: # noqa: SIM102
629+
if params.elements is not None:
630+
elements_to_be_rendered += (
631+
[params.elements] if isinstance(params.elements, str) else params.elements
632+
)
633+
634+
# filter out cs without relevant elements
635+
coordinate_systems = _get_valid_cs(
636+
sdata=sdata,
637+
coordinate_systems=coordinate_systems,
638+
render_images="render_images" in render_cmds,
639+
render_labels="render_labels" in render_cmds,
640+
render_points="render_points" in render_cmds,
641+
render_shapes="render_shapes" in render_cmds,
642+
elements=elements_to_be_rendered,
643+
)
644+
579645
# set up canvas
580646
fig_params, scalebar_params = _prepare_params_plot(
581647
num_panels=len(coordinate_systems),
@@ -616,64 +682,70 @@ def show(
616682

617683
for cmd, params in render_cmds.items():
618684
if cmd == "render_images" and has_images:
619-
_render_images(
620-
sdata=sdata,
621-
render_params=params,
622-
coordinate_system=cs,
623-
ax=ax,
624-
fig_params=fig_params,
625-
scalebar_params=scalebar_params,
626-
legend_params=legend_params,
627-
)
628685
wants_images = True
629686
wanted_images = params.elements if params.elements is not None else list(sdata.images.keys())
630-
wanted_elements.extend(
631-
[
632-
image
633-
for image in wanted_images
634-
if cs in set(get_transformation(sdata.images[image], get_all=True).keys())
635-
]
636-
)
687+
wanted_images_on_this_cs = [
688+
image
689+
for image in wanted_images
690+
if cs in set(get_transformation(sdata.images[image], get_all=True).keys())
691+
]
692+
wanted_elements.extend(wanted_images_on_this_cs)
693+
if len(wanted_images_on_this_cs) > 0:
694+
rasterize = (params.scale is None) or (
695+
isinstance(params.scale, str)
696+
and params.scale != "full"
697+
and (dpi is not None or figsize is not None)
698+
)
699+
_render_images(
700+
sdata=sdata,
701+
render_params=params,
702+
coordinate_system=cs,
703+
ax=ax,
704+
fig_params=fig_params,
705+
scalebar_params=scalebar_params,
706+
legend_params=legend_params,
707+
rasterize=rasterize,
708+
)
637709

638710
elif cmd == "render_shapes" and has_shapes:
639-
_render_shapes(
640-
sdata=sdata,
641-
render_params=params,
642-
coordinate_system=cs,
643-
ax=ax,
644-
fig_params=fig_params,
645-
scalebar_params=scalebar_params,
646-
legend_params=legend_params,
647-
)
648711
wants_shapes = True
649712
wanted_shapes = params.elements if params.elements is not None else list(sdata.shapes.keys())
650-
wanted_elements.extend(
651-
[
652-
shape
653-
for shape in wanted_shapes
654-
if cs in set(get_transformation(sdata.shapes[shape], get_all=True).keys())
655-
]
656-
)
713+
wanted_shapes_on_this_cs = [
714+
shape
715+
for shape in wanted_shapes
716+
if cs in set(get_transformation(sdata.shapes[shape], get_all=True).keys())
717+
]
718+
wanted_elements.extend(wanted_shapes_on_this_cs)
719+
if len(wanted_shapes_on_this_cs) > 0:
720+
_render_shapes(
721+
sdata=sdata,
722+
render_params=params,
723+
coordinate_system=cs,
724+
ax=ax,
725+
fig_params=fig_params,
726+
scalebar_params=scalebar_params,
727+
legend_params=legend_params,
728+
)
657729

658730
elif cmd == "render_points" and has_points:
659-
_render_points(
660-
sdata=sdata,
661-
render_params=params,
662-
coordinate_system=cs,
663-
ax=ax,
664-
fig_params=fig_params,
665-
scalebar_params=scalebar_params,
666-
legend_params=legend_params,
667-
)
668731
wants_points = True
669732
wanted_points = params.elements if params.elements is not None else list(sdata.points.keys())
670-
wanted_elements.extend(
671-
[
672-
point
673-
for point in wanted_points
674-
if cs in set(get_transformation(sdata.points[point], get_all=True).keys())
675-
]
676-
)
733+
wanted_points_on_this_cs = [
734+
point
735+
for point in wanted_points
736+
if cs in set(get_transformation(sdata.points[point], get_all=True).keys())
737+
]
738+
wanted_elements.extend(wanted_points_on_this_cs)
739+
if len(wanted_points_on_this_cs) > 0:
740+
_render_points(
741+
sdata=sdata,
742+
render_params=params,
743+
coordinate_system=cs,
744+
ax=ax,
745+
fig_params=fig_params,
746+
scalebar_params=scalebar_params,
747+
legend_params=legend_params,
748+
)
677749

678750
elif cmd == "render_labels" and has_labels:
679751
if sdata.table is not None and isinstance(params.color, str):
@@ -685,24 +757,30 @@ def show(
685757
key=params.color,
686758
palette=params.palette,
687759
)
688-
_render_labels(
689-
sdata=sdata,
690-
render_params=params,
691-
coordinate_system=cs,
692-
ax=ax,
693-
fig_params=fig_params,
694-
scalebar_params=scalebar_params,
695-
legend_params=legend_params,
696-
)
697760
wants_labels = True
698761
wanted_labels = params.elements if params.elements is not None else list(sdata.labels.keys())
699-
wanted_elements.extend(
700-
[
701-
label
702-
for label in wanted_labels
703-
if cs in set(get_transformation(sdata.labels[label], get_all=True).keys())
704-
]
705-
)
762+
wanted_labels_on_this_cs = [
763+
label
764+
for label in wanted_labels
765+
if cs in set(get_transformation(sdata.labels[label], get_all=True).keys())
766+
]
767+
wanted_elements.extend(wanted_labels_on_this_cs)
768+
if len(wanted_labels_on_this_cs) > 0:
769+
rasterize = (params.scale is None) or (
770+
isinstance(params.scale, str)
771+
and params.scale != "full"
772+
and (dpi is not None or figsize is not None)
773+
)
774+
_render_labels(
775+
sdata=sdata,
776+
render_params=params,
777+
coordinate_system=cs,
778+
ax=ax,
779+
fig_params=fig_params,
780+
scalebar_params=scalebar_params,
781+
legend_params=legend_params,
782+
rasterize=rasterize,
783+
)
706784

707785
if title is None:
708786
t = cs

0 commit comments

Comments
 (0)