Skip to content

Commit

Permalink
feat(core.plotting): adds a scale bar and a watermark to Bscan and Fu…
Browse files Browse the repository at this point in the history
…ndus visualizations
  • Loading branch information
Oli4 committed Mar 27, 2023
1 parent f37f20f commit 0e3eaa0
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 29 deletions.
82 changes: 73 additions & 9 deletions src/eyepy/core/eyebscan.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 = []
Expand Down Expand Up @@ -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
62 changes: 55 additions & 7 deletions src/eyepy/core/eyeenface.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
17 changes: 14 additions & 3 deletions src/eyepy/core/eyevolume.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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())
Expand Down
105 changes: 105 additions & 0 deletions src/eyepy/core/plotting.py
Original file line number Diff line number Diff line change
@@ -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))
4 changes: 2 additions & 2 deletions src/eyepy/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit 0e3eaa0

Please sign in to comment.