Skip to content

Commit

Permalink
fix(EyeVolume): fix B-scan iteration; enable setting layer heights fr…
Browse files Browse the repository at this point in the history
…om EyeBscan
  • Loading branch information
Oli4 committed Feb 17, 2022
1 parent 117ef89 commit f982d68
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 125 deletions.
6 changes: 4 additions & 2 deletions eyepy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

from eyepy.core import (
EyeEnface,
EyeVolumeLayerAnnotation,
EyeVolume,
EyeBscan,
EyeData,
EyeMeta,
EyeVolumeMeta,
EyeBscanMeta,
EyeEnfaceMeta,
EyeVolumeVoxelAnnotation,
EyeVolumeLayerAnnotation,
)

from eyepy.io import (
Expand Down
5 changes: 2 additions & 3 deletions eyepy/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# from annotations import Annotation, LayerAnnotation
from .eyemeta import EyeMeta
from .eyemeta import EyeVolumeMeta, EyeBscanMeta, EyeEnfaceMeta
from .eyebscan import EyeBscan
from .eyedata import EyeData
from .eyevolume import EyeVolumeLayerAnnotation
from .eyevolume import EyeVolume, EyeVolumeVoxelAnnotation
from .eyevolume import EyeVolume, EyeVolumeVoxelAnnotation, EyeVolumeLayerAnnotation
from .eyeenface import EyeEnface
31 changes: 23 additions & 8 deletions eyepy/core/eyebscan.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,46 @@
import collections

import numpy as np
from eyepy import config
import matplotlib.pyplot as plt


class EyeBscanLayers:
def __init__(self, eyebscan):
self.index = eyebscan.index
self.volume = eyebscan._volume

def __getitem__(self, item):
return self.volume.layers[item].data[-(self.index + 1)]

def __setitem__(self, key, value):
self.volume.layers[key].data[-(self.index + 1), :] = value


class EyeBscan:
def __init__(self, volume: "EyeVolume", index: int):
self.index = index
self._volume = volume

self._bscan_layers = EyeBscanLayers(self)

@property
def meta(self):
return self._volume.meta["bscan_meta"][self.index]

@property
def name(self):
return self.meta["name"]

@property
def data(self):
return self._volume.data[self.index]

@property
def layers(self):
return {
key: val.height_map[-(self.index + 1)]
for key, val in self._volume.layers.items()
}
return self._bscan_layers
# self._volume.layers
# return collections.defaultdict(lambda : )
# return {
# key: val.height_map[-(self.index + 1)]
# for key, val in self._volume.layers.items()
# }

@property
def ascan_maps(self):
Expand Down
4 changes: 2 additions & 2 deletions eyepy/core/eyeenface.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ def scale_y(self):

@property
def size_x(self):
return self.meta["size_x"]
return self.shape[1]

@property
def size_y(self):
return self.meta["size_y"]
return self.shape[0]

@property
def laterality(self):
Expand Down
82 changes: 67 additions & 15 deletions eyepy/core/eyemeta.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
import os
from typing import MutableMapping
from typing import MutableMapping, List, Tuple


class EyeMeta(MutableMapping):
def __init__(self, *args, **kwargs):
"""The Meta object is a dict with additional functionalities.
The additional functionallities are:
1. A string representation suitable for printing the meta information.
An instance of the meta object can be created as you would create an
ordinary dictionary.
For example:
+ Meta({"SizeX": 512})
+ Meta(SizeX=512)
+ Meta([(SizeX, 512), (SizeY, 512)])
"""
self._store = dict()
self.update(dict(*args, **kwargs)) # use the free update to set keys

Expand All @@ -41,3 +27,69 @@ def __str__(self):

def __repr__(self):
return self.__str__()


class EyeEnfaceMeta(EyeMeta):
def __init__(self, scale_x: float, scale_y: float, scale_unit: str, **kwargs):
"""A dict with required keys to hold meta data for enface images of the eye
Args:
scale_x: Horizontal scale of the enface pixels
scale_y: Vertical scale of the enface pixels
scale_unit: Unit of the scale. e.g. µm if scale is given in µm/pixel
**kwargs:
"""
super().__init__(
scale_x=scale_x, scale_y=scale_y, scale_unit=scale_unit, **kwargs
)


class EyeBscanMeta(EyeMeta):
def __init__(
self,
start_pos: Tuple[float, float],
end_pos: Tuple[float, float],
pos_unit: str,
**kwargs,
):
"""A dict with required keys to hold meta data for OCT B-scans
Args:
start_pos: B-scan start on the enface
end_pos: B-scan end on the enface
pos_unit: Unit of the positions
**kwargs:
"""
super().__init__(
start_pos=start_pos, end_pos=end_pos, pos_unit=pos_unit, **kwargs
)


class EyeVolumeMeta(EyeMeta):
def __init__(
self,
scale_z: float,
scale_x: float,
scale_y: float,
scale_unit: str,
bscan_meta: List[EyeBscanMeta],
**kwargs,
):
"""A dict with required keys to hold meta data for OCT volumes
Args:
scale_z: Distance between neighbouring B-scans
scale_x: Horizontal scale of the B-scan pixels
scale_y: Vertical scale of the B-scan pixels
scale_unit: Unit of the scale. e.g. µm if scale is given in µm/pixel
bscan_meta: A list holding an EyeBscanMeta object for every B-scan of the volume
**kwargs:
"""
super().__init__(
scale_z=scale_z,
scale_x=scale_x,
scale_y=scale_y,
scale_unit=scale_unit,
bscan_meta=bscan_meta,
**kwargs,
)
123 changes: 89 additions & 34 deletions eyepy/core/eyevolume.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,55 @@
import numpy as np
import numpy.typing as npt
from matplotlib import cm, colors, patches
from mpl_toolkits.axes_grid1 import make_axes_locatable
from numpy import typing as npt
from skimage import transform

from eyepy.core.eyeenface import EyeEnface
from eyepy.core.eyebscan import EyeBscan
from eyepy.core.eyemeta import EyeMeta
from eyepy.core.eyemeta import EyeEnfaceMeta, EyeBscanMeta, EyeVolumeMeta

from eyepy import config
from collections import defaultdict
from typing import Union, List
from typing import Union, List, Optional, Dict, TypedDict, Tuple
from skimage.transform._geometric import GeometricTransform

import matplotlib.pyplot as plt


class LayerKnot(TypedDict):
pos: Tuple[float, float]
cp_in: Tuple[float, float]
cp_out: Tuple[float, float]


class EyeVolumeLayerAnnotation:
def __init__(self, height_map, name, knots=None):
self.height_map = height_map
self.name = name
def __init__(
self,
volume: "EyeVolume",
data: Optional[npt.NDArray[np.float32]] = None,
knots: Optional[Dict[int, List[LayerKnot]]] = None,
):
"""
Args:
volume:
data: Layer height map
knots: Dict with List of CubicSpline knots for every B-scan, accessed by B-scan index
"""
self.volume = volume
if data is None:
self.data = np.full((volume.size_z, volume.size_x), np.nan)
else:
self.data = data

if knots is None:
self.knots = defaultdict(lambda: [])
elif type(knots) is dict:
self.knots = defaultdict(lambda: [], knots)

def layer_indices(self):
layer = self.height_map
layer = self.data
nan_indices = np.isnan(layer)
col_indices = np.arange(len(layer))[~nan_indices]
row_indices = np.rint(layer).astype(int)[~nan_indices]
Expand Down Expand Up @@ -228,7 +252,10 @@ def plot_quantification(
mask_img = np.zeros(self.volume.localizer.shape, dtype=float)[region]
visible = np.zeros_like(mask_img)
for mask_name in self.masks.keys():
mask_img += self.masks[mask_name][region] * self.quantification[mask_name]
mask_img += (
self.masks[mask_name][region]
* self.quantification[mask_name + " [mm³]"]
)
visible += self.masks[mask_name][region]

if vmin is None:
Expand Down Expand Up @@ -256,20 +283,21 @@ def plot_quantification(
class EyeVolume:
def __init__(
self,
data,
meta,
layers=None,
data: npt.NDArray[np.float32],
meta: EyeVolumeMeta = None,
ascan_maps=None,
localizer: "EyeEnface" = None,
transformation: GeometricTransform = None,
):
self.data = data
self.meta = meta
if layers is None:
self.layers = {}
self._bscans = {}

if meta is None:
self.meta = self._default_meta(self.data)
else:
self.layers = layers
self.meta = meta

self.layers = defaultdict(lambda: EyeVolumeLayerAnnotation(self))
self.volume_maps = {}

if ascan_maps is None:
Expand All @@ -283,25 +311,38 @@ def __init__(
self.localizer_transform = transformation

if localizer is None:
projection = np.flip(np.nanmean(data, axis=1), axis=0)
image = transform.warp(
projection,
self.localizer_transform.inverse,
output_shape=(self.size_x, self.size_x),
order=1,
)
self.localizer = EyeEnface(
image,
meta=EyeMeta(
size_x=self.size_x,
size_y=self.size_x,
scale_x=self.scale_x,
scale_y=self.scale_x,
),
)
self.localizer = self._default_localizer(self.data)
else:
self.localizer = localizer

def _default_meta(self, volume):
bscan_meta = [
EyeBscanMeta(
start_pos=(0, i), end_pos=((volume.shape[2] - 1), i), pos_unit="pixel"
)
for i in range(volume.shape[0] - 1, -1, -1)
]
meta = EyeVolumeMeta(
scale_x=1, scale_y=1, scale_z=1, scale_unit="pixel", bscan_meta=bscan_meta
)
return meta

def _default_localizer(self, data):
projection = np.flip(np.nanmean(data, axis=1), axis=0)
image = transform.warp(
projection,
self.localizer_transform.inverse,
output_shape=(self.size_x, self.size_x),
order=1,
)
localizer = EyeEnface(
image,
meta=EyeEnfaceMeta(
scale_x=self.scale_x, scale_y=self.scale_x, scale_unit="mm"
),
)
return localizer

def _estimate_transform(self):
"""Compute a transform to map a 2D projection of the volume to a square"""
# Points in oct space
Expand Down Expand Up @@ -333,20 +374,34 @@ def __getitem__(self, index) -> Union[EyeBscan, List[EyeBscan]]:
"""The B-Scan at the given index."""
if type(index) == slice:
return [self[i] for i in range(*index.indices(len(self)))]

if index < 0:
index = len(self) + index

if index < len(self):
try:
return self._bscans[index]
except KeyError:
self._bscans[index] = EyeBscan(self, index)
return self._bscans[index]
else:
if index <= len(self):
return EyeBscan(self, index)
else:
raise IndexError()
raise IndexError()

def __len__(self):
"""The number of B-Scans."""
return self.shape[0]

def add_layer(self, name, height_map):
self.layers[name] = EyeVolumeLayerAnnotation(self, height_map)

@property
def shape(self):
return self.data.shape

@property
def scale(self):
return self.scale_z, self.scale_y, self.scale_x

@property
def size_z(self):
return self.shape[0]
Expand Down

0 comments on commit f982d68

Please sign in to comment.