Skip to content

eyepy.core.eyevolume

EyeVolume(data, meta=None, localizer=None, transformation=None)

Parameters:

Name Type Description Default
data NDArray[float32]

A 3D numpy array containing the OCT data in shape (n_bscans, bscan_height, bscan_width)

required
meta Optional[EyeVolumeMeta]

Optional EyeVolumeMeta object.

None
localizer Optional[EyeEnface]
None
transformation Optional[_GeometricTransform]
None
Source code in src/eyepy/core/eyevolume.py
def __init__(
    self,
    data: npt.NDArray[np.float32],
    meta: Optional[EyeVolumeMeta] = None,
    localizer: Optional[EyeEnface] = None,
    transformation: Optional[_GeometricTransform] = None,
) -> None:
    """

    Args:
        data: A 3D numpy array containing the OCT data in shape (n_bscans, bscan_height, bscan_width)
        meta: Optional [EyeVolumeMeta][eyepy.core.eyemeta.EyeVolumeMeta] object.
        localizer:
        transformation:
    """
    self._raw_data = data
    self._data = None

    self._bscans = {}

    if meta is None:
        self.meta = self._default_meta(self._raw_data)
    else:
        self.meta = meta
    if not 'intensity_transform' in self.meta:
        self.meta['intensity_transform'] = 'default'

    self.set_intensity_transform(self.meta['intensity_transform'])

    self._layers = []
    self._volume_maps = []
    self._ascan_maps = []

    if transformation is None:
        self.localizer_transform = self._estimate_transform()
    else:
        self.localizer_transform = transformation

    if localizer is None:
        self.localizer = self._default_localizer(self.data)
    else:
        self.localizer = localizer

data: np.ndarray property

Returns:

laterality: str property writable

Returns:

layers: dict[str, EyeVolumeLayerAnnotation] property

Returns:

scale: tuple[float, float, float] property writable

Returns:

scale_unit: str property writable

Returns:

scale_x: float property writable

Returns:

scale_y: float property writable

Returns:

scale_z: float property writable

Returns:

shape: tuple[int, int, int] property writable

Returns:

size_x: int property writable

Returns:

size_y: int property writable

Returns:

size_z: int property writable

Returns:

volume_maps: dict[str, EyeVolumePixelAnnotation] property

Returns:

__getitem__(index)

Parameters:

Name Type Description Default
index Union[SupportsIndex, slice]
required

Returns:

Source code in src/eyepy/core/eyevolume.py
def __getitem__(
        self, index: Union[SupportsIndex,
                           slice]) -> Union[list[EyeBscan], EyeBscan]:
    """

    Args:
        index:

    Returns:

    """
    # The B-Scan at the given index.
    if isinstance(index, slice):
        return [self[i] for i in range(*index.indices(len(self)))]
    elif isinstance(index, int):
        if index < 0:
            index = len(self) + index

        if index < len(self):
            try:
                # Return B-scan with type annotation
                return self._bscans[index]
            except KeyError:
                self._bscans[index] = EyeBscan(self, index)
                return self._bscans[index]
        else:
            raise IndexError()
    else:
        raise TypeError()

__len__()

The number of B-Scans.

Source code in src/eyepy/core/eyevolume.py
def __len__(self) -> int:
    """The number of B-Scans."""
    return self.shape[0]

add_layer_annotation(height_map=None, meta=None, **kwargs)

Parameters:

Name Type Description Default
height_map Optional[NDArray[float_]]

Height in shape (n_Bscans, Bscan_width) The first index refers to the bottom most B-scan

None
meta Optional[dict]

name, current_color, and knots

None
**kwargs
{}

Returns:

Source code in src/eyepy/core/eyevolume.py
def add_layer_annotation(self,
                         height_map: Optional[npt.NDArray[
                             np.float_]] = None,
                         meta: Optional[dict] = None,
                         **kwargs) -> EyeVolumeLayerAnnotation:
    """

    Args:
        height_map: Height in shape (n_Bscans, Bscan_width) The first index refers to the bottom most B-scan
        meta: name, current_color, and knots
        **kwargs:

    Returns:

    """
    if meta is None:
        meta = {}
    meta.update(**kwargs)
    layer_annotation = EyeVolumeLayerAnnotation(self, height_map, **meta)
    self._layers.append(layer_annotation)
    return layer_annotation

add_pixel_annotation(voxel_map=None, meta=None, **kwargs)

Parameters:

Name Type Description Default
voxel_map Optional[NDArray[bool_]]
None
meta Optional[dict]
None
**kwargs
{}

Returns:

Source code in src/eyepy/core/eyevolume.py
def add_pixel_annotation(self,
                         voxel_map: Optional[npt.NDArray[np.bool_]] = None,
                         meta: Optional[dict] = None,
                         **kwargs) -> EyeVolumePixelAnnotation:
    """

    Args:
        voxel_map:
        meta:
        **kwargs:

    Returns:

    """
    if meta is None:
        meta = {}
    meta.update(**kwargs)
    voxel_annotation = EyeVolumePixelAnnotation(self, voxel_map, **meta)
    self._volume_maps.append(voxel_annotation)
    return voxel_annotation

load(path) classmethod

Parameters:

Name Type Description Default
path Union[str, Path]
required

Returns:

Source code in src/eyepy/core/eyevolume.py
@classmethod
def load(cls, path: Union[str, Path]) -> 'EyeVolume':
    """

    Args:
        path:

    Returns:

    """
    with tempfile.TemporaryDirectory() as tmpdirname:
        tmpdirname = Path(tmpdirname)
        with zipfile.ZipFile(path, 'r') as zip_ref:
            zip_ref.extractall(tmpdirname)

        # Load raw volume and meta
        data = np.load(tmpdirname / 'raw_volume.npy')
        with open(tmpdirname / 'meta.json', 'r') as meta_file:
            volume_meta = EyeVolumeMeta.from_dict(json.load(meta_file))

        # Load Volume Annotations
        voxels_path = tmpdirname / 'annotations' / 'voxels'
        if voxels_path.exists():
            voxel_annotations = np.load(voxels_path / 'voxel_maps.npy')
            with open(voxels_path / 'meta.json', 'r') as meta_file:
                voxels_meta = json.load(meta_file)
        else:
            voxel_annotations = []
            voxels_meta = []

        # Load layers
        layers_path = tmpdirname / 'annotations' / 'layers'
        if layers_path.exists():
            layer_annotations = np.load(layers_path / 'layer_heights.npy')
            with open(layers_path / 'meta.json', 'r') as meta_file:
                layers_meta = json.load(meta_file)

            # Clean knots
            for i, layer_meta in enumerate(layers_meta):
                if 'knots' in layer_meta:
                    knots = layer_meta['knots']
                    knots = {int(i): knots[i] for i in knots}
                    layer_meta['knots'] = knots
        else:
            layer_annotations = []
            layers_meta = []

        # Load Localizer and meta
        localizer_path = tmpdirname / 'localizer'
        localizer_data = np.load(localizer_path / 'localizer.npy')
        with open(localizer_path / 'meta.json', 'r') as meta_file:
            localizer_meta = EyeEnfaceMeta.from_dict(json.load(meta_file))
        localizer = EyeEnface(data=localizer_data, meta=localizer_meta)

        # Load Localizer Annotations
        pixels_path = localizer_path / 'annotations' / 'pixel'
        if pixels_path.exists():
            pixel_annotations = np.load(pixels_path / 'pixel_maps.npy')
            with open(pixels_path / 'meta.json', 'r') as meta_file:
                pixels_meta = json.load(meta_file)

            for i, pixel_meta in enumerate(pixels_meta):
                localizer.add_area_annotation(pixel_annotations[i],
                                              pixel_meta)

        from eyepy.io.utils import _compute_localizer_oct_transform

        transformation = _compute_localizer_oct_transform(
            volume_meta, localizer_meta, data.shape)

        ev = cls(
            data=data,
            meta=volume_meta,
            localizer=localizer,
            transformation=transformation,
        )
        for meta, annotation in zip(layers_meta, layer_annotations):
            ev.add_layer_annotation(annotation, meta)

        for meta, annotation in zip(voxels_meta, voxel_annotations):
            ev.add_pixel_annotation(annotation, meta)

    return ev

plot(ax=None, projections=False, bscan_region=False, bscan_positions=False, quantification=None, region=np.s_[:, :], annotations_only=False, projection_kwargs=None, line_kwargs=None, scalebar='botleft', scalebar_kwargs=None, watermark=True)

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.

Parameters:

Name Type Description Default
ax Optional[Axes]

Axes to plot on. If not provided plot on the current axes (plt.gca()).

None
projections Union[bool, list[str]]

If True plot all projections (default: False). If a list of strings is given, plot the projections with the given names. Projections are 2D enface views on oct volume annotations such as drusen.

False
bscan_region bool

If True plot the region B-scans are located in (default: False)

False
bscan_positions Union[bool, list[int]]

If True plot positions of all B-scan (default: False). If a list of integers is given, plot the B-scans with the respective indices. Indexing starts at the bottom of the localizer.

False
quantification Optional[str]

Name of the OCT volume annotations to plot a quantification for (default: None). Quantifications are performed on circular grids.

None
region tuple[slice, slice]

Region of the localizer to plot (default: np.s_[...])

s_[:, :]
annotations_only bool

If True localizer image is not plotted (defaualt: False)

False
projection_kwargs Optional[dict]

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.

None
line_kwargs Optional[dict]

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"}

None
scalebar Union[bool, str]

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.

'botleft'
scalebar_kwargs Optional[dict]

Optional keyword arguments for customizing the scalebar. Check the documentation of plot_scalebar for more information.

None
watermark bool

If True plot a watermark on the image (default: True). When removing the watermark, please consider to cite eyepy in your publication.

True

Returns: None

Source code in src/eyepy/core/eyevolume.py
def plot(
    self,
    ax: Optional[plt.Axes] = None,
    projections: Union[bool, list[str]] = False,
    bscan_region: bool = False,
    bscan_positions: Union[bool, list[int]] = False,
    quantification: Optional[str] = None,
    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.

    Args:
        ax: Axes to plot on. If not provided plot on the current axes (plt.gca()).
        projections: If `True` plot all projections (default: `False`). If a list of strings is given, plot the projections with the given names. Projections are 2D enface views on oct volume annotations such as drusen.
        bscan_region: If `True` plot the region B-scans are located in (default: `False`)
        bscan_positions: If `True` plot positions of all B-scan (default: `False`). If a list of integers is given, plot the B-scans with the respective indices. Indexing starts at the bottom of the localizer.
        quantification: Name of the OCT volume annotations to plot a quantification for (default: `None`). Quantifications are performed on circular grids.
        region: Region of the localizer to plot (default: `np.s_[...]`)
        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
    """

    # Complete region index expression
    y_start = region[0].start if region[0].start is not None else 0
    y_stop = region[0].stop if region[
        0].stop is not None else self.localizer.shape[0]
    x_start = region[1].start if region[1].start is not None else 0
    x_stop = region[1].stop if region[
        1].stop is not None else self.localizer.shape[1]

    region = np.s_[y_start:y_stop, x_start:x_stop]

    if ax is None:
        ax = plt.gca()

    if not annotations_only:
        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())
    elif not projections:
        projections = []

    if projection_kwargs is None:
        projection_kwargs = defaultdict(lambda: {})
    for name in projections:
        if not name in projection_kwargs.keys():
            projection_kwargs[name] = {}
        self.volume_maps[name].plot(ax=ax,
                                    region=region,
                                    **projection_kwargs[name])

    if line_kwargs is None:
        line_kwargs = config.line_kwargs
    else:
        line_kwargs = {**config.line_kwargs, **line_kwargs}

    if bscan_positions:
        self._plot_bscan_positions(
            ax=ax,
            bscan_positions=bscan_positions,
            region=region,
            line_kwargs=line_kwargs,
        )
    if bscan_region:
        self._plot_bscan_region(region=region,
                                ax=ax,
                                line_kwargs=line_kwargs)

    if quantification:
        self.volume_maps[quantification].plot_quantification(region=region,
                                                             ax=ax)

remove_layer_annotation(name)

Parameters:

Name Type Description Default
name str
required

Returns:

Source code in src/eyepy/core/eyevolume.py
def remove_layer_annotation(self, name: str) -> None:
    """

    Args:
        name:

    Returns:

    """
    for i, layer in enumerate(self._layers):
        if layer.name == name:
            self._layers.pop(i)

    # Remove references from B-scans
    for bscan in self:
        if name in bscan.layers:
            bscan.layers.pop(name)

remove_pixel_annotation(name)

Parameters:

Name Type Description Default
name str
required

Returns:

Source code in src/eyepy/core/eyevolume.py
def remove_pixel_annotation(self, name: str) -> None:
    """

    Args:
        name:

    Returns:

    """
    for i, voxel_map in enumerate(self._volume_maps):
        if voxel_map.name == name:
            self._volume_maps.pop(i)

    # Remove references from B-scans
    for bscan in self:
        if name in bscan.area_maps:
            bscan.area_maps.pop(name)

save(path)

Parameters:

Name Type Description Default
path Union[str, Path]
required

Returns:

Source code in src/eyepy/core/eyevolume.py
def save(self, path: Union[str, Path]) -> None:
    """

    Args:
        path:

    Returns:

    """
    # Create temporary folder
    with tempfile.TemporaryDirectory() as tmpdirname:
        tmpdirname = Path(tmpdirname)

        # Save OCT volume as npy and meta as json
        np.save(tmpdirname / 'raw_volume.npy', self._raw_data)
        with open(tmpdirname / 'meta.json', 'w') as meta_file:
            if self.meta['intensity_transform'] == 'custom':
                warnings.warn(
                    'Custom intensity transforms can not be saved.')
                self.meta['intensity_transform'] = 'default'
            json.dump(self.meta.as_dict(), meta_file)

        if not len(self._volume_maps) == 0:
            # Save Volume Annotations
            voxels_path = tmpdirname / 'annotations' / 'voxels'
            voxels_path.mkdir(exist_ok=True, parents=True)
            np.save(
                voxels_path / 'voxel_maps.npy',
                np.stack([v.data for v in self._volume_maps]),
            )
            with open(voxels_path / 'meta.json', 'w') as meta_file:
                json.dump([v.meta for v in self._volume_maps], meta_file)

        if not len(self._layers) == 0:
            # Save layer annotations
            layers_path = tmpdirname / 'annotations' / 'layers'
            layers_path.mkdir(exist_ok=True, parents=True)
            np.save(
                layers_path / 'layer_heights.npy',
                np.stack([l.data for l in self._layers]),
            )
            with open(layers_path / 'meta.json', 'w') as meta_file:
                json.dump([l.meta for l in self._layers], meta_file)

        # Save Localizer
        localizer_path = tmpdirname / 'localizer'
        localizer_path.mkdir(exist_ok=True, parents=True)
        np.save(localizer_path / 'localizer.npy', self.localizer.data)
        with open(localizer_path / 'meta.json', 'w') as meta_file:
            json.dump(self.localizer.meta.as_dict(), meta_file)

        # Save Localizer Annotations
        if not len(self.localizer._area_maps) == 0:
            pixels_path = localizer_path / 'annotations' / 'pixel'
            pixels_path.mkdir(exist_ok=True, parents=True)
            np.save(
                pixels_path / 'pixel_maps.npy',
                np.stack([p.data for p in self.localizer._area_maps]),
            )
            with open(pixels_path / 'meta.json', 'w') as meta_file:
                json.dump([p.meta for p in self.localizer._area_maps],
                          meta_file)

        # Zip and copy to location
        name = shutil.make_archive(str(path),
                                   'zip',
                                   root_dir=str(tmpdirname))
        # Remove zip extension
        shutil.move(name, path)

set_intensity_transform(func)

Parameters:

Name Type Description Default
func Union[str, Callable]

Either a string specifying a transform from eyepy.core.utils.intensity_transforms or a function

required

Returns:

Source code in src/eyepy/core/eyevolume.py
def set_intensity_transform(self, func: Union[str, Callable]) -> None:
    """

    Args:
        func: Either a string specifying a transform from eyepy.core.utils.intensity_transforms or a function

    Returns:

    """
    if isinstance(func, str):
        if func in intensity_transforms:
            self.meta['intensity_transform'] = func
            self.intensity_transform = intensity_transforms[func]
            self._data = None
        elif func == 'custom':
            logger.warning(
                'Custom intensity transforms can not be loaded currently')
        else:
            logger.warning(
                f"Provided intensity transform name {func} is not known. Valid names are 'vol' or 'default'. You can also pass your own function."
            )
    elif isinstance(func, Callable):
        self.meta['intensity_transform'] = 'custom'
        self.intensity_transform = func
        self._data = None