diff --git a/src/eyepy/core/eyebscan.py b/src/eyepy/core/eyebscan.py index c53a2d1..85107ce 100644 --- a/src/eyepy/core/eyebscan.py +++ b/src/eyepy/core/eyebscan.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List, Optional, Tuple, TYPE_CHECKING, Union +from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union import matplotlib.colors as mcolors import matplotlib.patches as mpatches @@ -10,6 +10,8 @@ from eyepy import config from eyepy.core.annotations import EyeBscanLayerAnnotation from eyepy.core.eyemeta import EyeBscanMeta +from eyepy.core.plotting import plot_scalebar +from eyepy.core.plotting import plot_watermark from eyepy.core.utils import DynamicDefaultDict if TYPE_CHECKING: @@ -84,8 +86,11 @@ def plot( layer_kwargs: Optional[dict] = None, area_kwargs: Optional[dict] = None, #ascan_kwargs=None, - annotations_only: bool=False, - region: Union[slice, Tuple[slice, slice]] = np.s_[:, :], + annotations_only: bool = False, + region: Tuple[slice, slice] = np.s_[:, :], + scalebar: Union[bool, str] = "botleft", + scalebar_kwargs: Optional[Dict[str, Any]] = None, + watermark: bool = True, ) -> None: """ Plot B-scan. @@ -96,16 +101,17 @@ def plot( layers: If `True` plot all layers (default: `False`). If a list of strings is given, plot the layers with the given names. areas: If `True` plot all areas (default: `False`). If a list of strings is given, plot the areas with the given names. annotations_only: If `True` do not plot the B-scan image - region: Region of the localizer to plot (default: `np.s_[...]`) + region: Region of the localizer to plot (default: `np.s_[:, :]`) layer_kwargs: Optional keyword arguments for customizing the OCT layers. If `None` default values are used which are {"linewidth": 1, "linestyle": "-"} area_kwargs: Optional keyword arguments for customizing area annotions on the B-scan If `None` default values are used which are {"alpha": 0.5} - + scalebar: Position of the scalebar, one of "topright", "topleft", "botright", "botleft" or `False` (default: "botleft"). If `True` the scalebar is placed in the bottom left corner. You can custumize the scalebar using the `scalebar_kwargs` argument. + scalebar_kwargs: Optional keyword arguments for customizing the scalebar. Check the documentation of [plot_scalebar][eyepy.core.plotting.plot_scalebar] for more information. + watermark: If `True` plot a watermark on the image (default: `True`). When removing the watermark, please consider to cite eyepy in your publication. Returns: None """ - if ax is None: - ax = plt.gca() + ax = plt.gca() if ax is None else ax # Complete region index expression y_start = region[0].start if region[0].start is not None else 0 @@ -118,12 +124,12 @@ def plot( if not layers: layers = [] elif layers is True: - layers = self.volume.layers.keys() + layers = list(self.volume.layers.keys()) if not areas: areas = [] elif areas is True: - areas = self.volume.volume_maps.keys() + areas = list(self.volume.volume_maps.keys()) #if ascans is None: # ascans = [] @@ -218,3 +224,61 @@ def plot( # Set labels to ticks + start of the region as an offset ax.set_yticklabels([str(int(t + y_start)) for t in yticks]) ax.set_xticklabels([str(int(t + x_start)) for t in xticks]) + + if scalebar: + if scalebar_kwargs is None: + scalebar_kwargs = {} + + scale_unit = self.volume.meta["scale_unit"] + scalebar_kwargs = { + **{ + "scale": (self.scale_x, self.scale_y), + "scale_unit": scale_unit + }, + **scalebar_kwargs + } + + if not "pos" in scalebar_kwargs: + sx = x_end - x_start + sy = y_end - y_start + + if scalebar is True: + scalebar = "botleft" + + if scalebar == "botleft": + scalebar_kwargs["pos"] = (sx - 0.95 * sx, 0.95 * sy) + elif scalebar == "botright": + scalebar_kwargs["pos"] = (0.95 * sx, 0.95 * sy) + scalebar_kwargs["flip_x"] = True + elif scalebar == "topleft": + scalebar_kwargs["pos"] = (sx - 0.95 * sx, 0.05 * sy) + scalebar_kwargs["flip_y"] = True + elif scalebar == "topright": + scalebar_kwargs["pos"] = (0.95 * sx, 0.05 * sy) + scalebar_kwargs["flip_x"] = True + scalebar_kwargs["flip_y"] = True + + plot_scalebar(ax=ax, **scalebar_kwargs) + + if watermark: + plot_watermark(ax) + + @property + def size_x(self): + """Size of the B-scan in x direction""" + return self.shape[1] + + @property + def size_y(self): + """Size of the B-scan in y direction""" + return self.shape[0] + + @property + def scale_x(self): + """Scale of the B-scan in x direction""" + return self.volume.scale_x + + @property + def scale_y(self): + """Scale of the B-scan in y direction""" + return self.volume.scale_y diff --git a/src/eyepy/core/eyeenface.py b/src/eyepy/core/eyeenface.py index ed9ce59..a8fb607 100644 --- a/src/eyepy/core/eyeenface.py +++ b/src/eyepy/core/eyeenface.py @@ -1,10 +1,12 @@ -from typing import Dict, Optional, Tuple, TYPE_CHECKING, Union +from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING, Union import matplotlib.pyplot as plt from numpy import typing as npt import numpy as np from eyepy.core.annotations import EyeEnfacePixelAnnotation +from eyepy.core.plotting import plot_scalebar +from eyepy.core.plotting import plot_watermark if TYPE_CHECKING: from eyepy import EyeEnfaceMeta @@ -110,16 +112,24 @@ def shape(self) -> Tuple[int, int]: """ return self.data.shape - def plot(self, - ax: Optional[plt.Axes] = None, - region: Union[slice, Tuple[slice, slice]] = np.s_[:, :]): + def plot( + self, + ax: Optional[plt.Axes] = None, + region: Tuple[slice, slice] = np.s_[:, :], + scalebar: Union[bool, str] = "botleft", + scalebar_kwargs: Optional[Dict[str, Any]] = None, + watermark: bool = True, + ) -> None: """ Args: - ax: - region: - + ax: Axes to plot on. If not provided plot on the current axes (plt.gca()). + region: Region of the localizer to plot (default: `np.s_[:, :]`) + scalebar: Position of the scalebar, one of "topright", "topleft", "botright", "botleft" or `False` (default: "botleft"). If `True` the scalebar is placed in the bottom left corner. You can custumize the scalebar using the `scalebar_kwargs` argument. + scalebar_kwargs: Optional keyword arguments for customizing the scalebar. Check the documentation of [plot_scalebar][eyepy.core.plotting.plot_scalebar] for more information. + watermark: If `True` plot a watermark on the image (default: `True`). When removing the watermark, please consider to cite eyepy in your publication. Returns: + None """ ax = plt.gca() if ax is None else ax @@ -146,3 +156,41 @@ def plot(self, # Set labels to ticks + start of the region as an offset ax.set_yticklabels([str(int(t + y_start)) for t in yticks]) ax.set_xticklabels([str(int(t + x_start)) for t in xticks]) + + if scalebar: + if scalebar_kwargs is None: + scalebar_kwargs = {} + + scale_unit = self.meta["scale_unit"] + scalebar_kwargs = { + **{ + "scale": (self.scale_x, self.scale_y), + "scale_unit": scale_unit + }, + **scalebar_kwargs + } + + if not "pos" in scalebar_kwargs: + sx = x_end - x_start + sy = y_end - y_start + + if scalebar is True: + scalebar = "botleft" + + if scalebar == "botleft": + scalebar_kwargs["pos"] = (sx - 0.95 * sx, 0.95 * sy) + elif scalebar == "botright": + scalebar_kwargs["pos"] = (0.95 * sx, 0.95 * sy) + scalebar_kwargs["flip_x"] = True + elif scalebar == "topleft": + scalebar_kwargs["pos"] = (sx - 0.95 * sx, 0.05 * sy) + scalebar_kwargs["flip_y"] = True + elif scalebar == "topright": + scalebar_kwargs["pos"] = (0.95 * sx, 0.05 * sy) + scalebar_kwargs["flip_x"] = True + scalebar_kwargs["flip_y"] = True + + plot_scalebar(ax=ax, **scalebar_kwargs) + + if watermark: + plot_watermark(ax) diff --git a/src/eyepy/core/eyevolume.py b/src/eyepy/core/eyevolume.py index 0cf52b2..62c64ca 100644 --- a/src/eyepy/core/eyevolume.py +++ b/src/eyepy/core/eyevolume.py @@ -24,6 +24,8 @@ from eyepy.core.eyemeta import EyeBscanMeta from eyepy.core.eyemeta import EyeEnfaceMeta from eyepy.core.eyemeta import EyeVolumeMeta +from eyepy.core.plotting import plot_scalebar +from eyepy.core.plotting import plot_watermark from eyepy.core.utils import intensity_transforms logger = logging.getLogger("eyepy.core.eyevolume") @@ -614,10 +616,13 @@ def plot( bscan_region: bool = False, bscan_positions: Union[bool, List[int]] = False, quantification: Optional[str] = None, - region: Union[slice, Tuple[slice, slice]] = np.s_[:, :], + region: Tuple[slice, slice] = np.s_[:, :], annotations_only: bool = False, projection_kwargs: Optional[dict] = None, line_kwargs: Optional[dict] = None, + scalebar: Union[bool, str] = "botleft", + scalebar_kwargs: Optional[dict] = None, + watermark: bool = True, ) -> None: """ Plot an annotated OCT localizer image. If the volume does not provide a localizer image an enface projection of the OCT volume is used instead. @@ -631,7 +636,9 @@ def plot( annotations_only: If `True` localizer image is not plotted (defaualt: `False`) projection_kwargs: Optional keyword arguments for the projection plots. If `None` default values are used (default: `None`). If a dictionary is given, the keys are the projection names and the values are dictionaries of keyword arguments. line_kwargs: Optional keyword arguments for customizing the lines to show B-scan region and positions plots. If `None` default values are used which are {"linewidth": 0.2, "linestyle": "-", "color": "green"} - + scalebar: Position of the scalebar, one of "topright", "topleft", "botright", "botleft" or `False` (default: "botleft"). If `True` the scalebar is placed in the bottom left corner. You can custumize the scalebar using the `scalebar_kwargs` argument. + scalebar_kwargs: Optional keyword arguments for customizing the scalebar. Check the documentation of [plot_scalebar][eyepy.core.plotting.plot_scalebar] for more information. + watermark: If `True` plot a watermark on the image (default: `True`). When removing the watermark, please consider to cite eyepy in your publication. Returns: None @@ -651,7 +658,11 @@ def plot( ax = plt.gca() if not annotations_only: - self.localizer.plot(ax=ax, region=region) + self.localizer.plot(ax=ax, + region=region, + scalebar=scalebar, + scalebar_kwargs=scalebar_kwargs, + watermark=watermark) if projections is True: projections = list(self.volume_maps.keys()) diff --git a/src/eyepy/core/plotting.py b/src/eyepy/core/plotting.py new file mode 100644 index 0000000..f33b2fd --- /dev/null +++ b/src/eyepy/core/plotting.py @@ -0,0 +1,105 @@ +from typing import Optional, Tuple, Union + +import matplotlib.pyplot as plt + + +def plot_scalebar(scale: Tuple[float, float], + scale_unit: str, + scale_length: Optional[Union[int, float]] = None, + pos: Tuple[int, int] = (100, 100), + flip_x: bool = False, + flip_y: bool = False, + color: str = "white", + linewidth: float = 1.5, + ax: Optional[plt.Axes] = None, + **kwargs: dict) -> None: + """ Plot a scalebar for an image + + Args: + scale: tuple of floats (x, y) with the scale in units per pixel. If the `scale_unit` is "px" the scale is ignored. + scale_unit: unit of the scalebar ("px" or "µm", or "mm") + scale_length: length of the scalebar in units + pos: position of the scalebar in pixels + flip_x: flip the scalebar in x direction + flip_y: flip the scalebar in y direction + color: color of the scalebar + linewidth: linewidth of the scalebar + ax: matplotlib axis to plot on + **kwargs: additional keyword arguments passed to ax.plot + Returns: + None + """ + ax = plt.gca() if ax is None else ax + + x, y = pos + + if scale_unit == "px": + scale = (1.0, 1.0) + + if scale_length is None: + if scale_unit == "px": + scale_length = 100 + elif scale_unit == "µm": + scale_length = 500 + elif scale_unit == "mm": + scale_length = 0.5 + + x_start = x + x_end = x + (scale_length / scale[0]) + + y_start = y + y_end = y - (scale_length / scale[1]) + + text_x = x + 8 + text_y = y - 8 + + if flip_x: + x_start = x - (scale_length / scale[0]) + x_end = x + text_x = x - 50 + + if flip_y: + y_start = y + (scale_length / scale[1]) + y_end = y + text_y = y + 17 + + # Plot horizontal line + ax.plot([x_start, x_end], [y, y], + color=color, + linewidth=linewidth, + **kwargs) + # Plot vertical line + ax.plot([x, x], [y_start, y_end], + color=color, + linewidth=linewidth, + **kwargs) + + ax.text(text_x, + text_y, + f"{scale_length}{scale_unit}", + fontsize=7, + weight="bold", + color=color) + + +def plot_watermark(ax: plt.Axes) -> None: + """ Add a watermark in the lower right corner of a matplotlib axes object + + Args: + ax: Axes object + Returns: + None + """ + ax.text(0.98, + 0.02, + "Visualized with eyepy", + fontsize=6, + color='white', + ha='right', + va='bottom', + alpha=0.4, + transform=plt.gca().transAxes, + bbox=dict(boxstyle="Round", + facecolor='gray', + alpha=0.2, + linewidth=0)) diff --git a/src/eyepy/core/utils.py b/src/eyepy/core/utils.py index 3e7e1ba..2be1c46 100644 --- a/src/eyepy/core/utils.py +++ b/src/eyepy/core/utils.py @@ -2,8 +2,8 @@ import numpy as np import numpy.typing as npt -from skimage import img_as_float32 -from skimage import img_as_ubyte +from skimage.util import img_as_float32 +from skimage.util import img_as_ubyte from eyepy.core.filter import filter_by_height_enface diff --git a/src/eyepy/io/he/e2e_reader.py b/src/eyepy/io/he/e2e_reader.py index 8c3ead3..46c556b 100644 --- a/src/eyepy/io/he/e2e_reader.py +++ b/src/eyepy/io/he/e2e_reader.py @@ -541,9 +541,9 @@ def localizer_meta(self) -> EyeEnfaceMeta: """Return EyeEnfaceMeta object for the localizer image.""" if self._localizer_meta is None: self._localizer_meta = EyeEnfaceMeta( - scale_x=0.0114, # Todo: Where is this in E2E? - scale_y=0.0114, # Todo: Where is this in E2E? - scale_unit="mm", + scale_x=1, #0.0114, # Todo: Where is this in E2E? + scale_y=1, #0.0114, # Todo: Where is this in E2E? + scale_unit="px", modality=self.enface_modality(), laterality=self.laterality(), field_size=None, @@ -594,11 +594,11 @@ def get_meta(self) -> EyeVolumeMeta: if self._meta is None: bscan_meta = self.get_bscan_meta() self._meta = EyeVolumeMeta( - scale_x=0.0114, # Todo: Where is this in E2E? - scale_y=bscan_meta[0]["scale_y"], - scale_z=get_bscan_spacing(bscan_meta) if - (bscan_meta[0]["scan_pattern"] not in [1, 2]) else 0.03, - scale_unit="mm", + scale_x=1, #0.0114, # Todo: Where is this in E2E? + scale_y=1, #bscan_meta[0]["scale_y"], + scale_z=1, #get_bscan_spacing(bscan_meta) if + #(bscan_meta[0]["scan_pattern"] not in [1, 2]) else 0.03, + scale_unit="px", laterality=self.laterality, visit_date=None, exam_time=None,