Skip to content

Commit

Permalink
kymo: add downsampled_by in position
Browse files Browse the repository at this point in the history
  • Loading branch information
JoepVanlier committed May 19, 2021
1 parent 85d57f5 commit b180068
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 21 deletions.
2 changes: 1 addition & 1 deletion changelog.md
Expand Up @@ -4,7 +4,7 @@

#### New features

* Added `Kymo.downsampled_by()` for downsampling Kymographs. See [kymographs](https://lumicks-pylake.readthedocs.io/en/latest/tutorial/kymographs.html) for more information.
* Added `Kymo.downsampled_by()` for downsampling Kymographs in space and time. See [kymographs](https://lumicks-pylake.readthedocs.io/en/latest/tutorial/kymographs.html) for more information.
* Added option to stitch Kymograph lines via the Jupyter notebook widget.
* Added Mean Square Displacement (MSD) and diffusion constant estimation to `KymoLine`. For more information, please refer to [kymotracking](https://lumicks-pylake.readthedocs.io/en/latest/tutorial/kymotracking.html)
* Added `FdCurve.with_baseline_corrected_x()` to return a baseline corrected version of the FD curve if the corrected data is available. **Note: currently the baseline is only calculated for the x-component of the force channel in Bluelake. Therefore baseline corrected `FdCurve` instances use only the x-component of the force channel, unlike default `FdCurve`s which use the full magnitude of the force channel by default.**
Expand Down
14 changes: 11 additions & 3 deletions docs/tutorial/kymographs.rst
Expand Up @@ -52,15 +52,23 @@ There are also several properties available for convenient access to the kymogra
Downsampling kymograph
----------------------

We can downsample a kymograph by invoking::
We can downsample a kymograph in time by invoking::

kymo_ds = kymo.downsampled_by(time_factor=2)

Or in space by invoking::

kymo_ds = kymo.downsampled_by(position_factor=2)

Or both::

kymo_ds = kymo.downsampled_by(time_factor=2, position_factor=2)

Note however, that not all functionalities are present anymore when downsampling a kymograph. For
example, we can no longer access the per pixel timestamps::
example, if we downsample a kymograph by time, we can no longer access the per pixel timestamps::

>>> kymo_ds.timestamps
AttributeError: Per pixel timestamps are no longer available after downsampling a kymograph since they are not well defined (the downsampling occurs over a non contiguous time window).
AttributeError: Per pixel timestamps are no longer available after downsampling a kymograph in time since they are not well defined (the downsampling occurs over a non contiguous time window).
Line timestamps are still available however. See: `Kymo.line_time_seconds`.

Plotting and exporting
Expand Down
24 changes: 19 additions & 5 deletions lumicks/pylake/detail/confocal.py
Expand Up @@ -26,6 +26,14 @@ def _default_timestamp_factory(self: "ConfocalImage"):
return self._to_spatial(raw_image)


def _default_pixelsize_factory(self: "ConfocalImage"):
return [axes["pixel size (nm)"] / 1000 for axes in self._ordered_axes()]


def _default_pixelcount_factory(self: "ConfocalImage"):
return [axes["num of pixels"] for axes in self._ordered_axes()]


class BaseScan(PhotonCounts, ExcitationLaserPower):
"""Base class for confocal scans
Expand All @@ -51,6 +59,8 @@ def __init__(self, name, file, start, stop, json):
self.file = file
self._image_factory = _default_image_factory
self._timestamp_factory = _default_timestamp_factory
self._pixelsize_factory = _default_pixelsize_factory
self._pixelcount_factory = _default_pixelcount_factory
self._cache = {}

def _has_default_factories(self):
Expand Down Expand Up @@ -273,7 +283,7 @@ def pixels_per_line(self):

@property
def _num_pixels(self):
return [axes["num of pixels"] for axes in self._ordered_axes()]
return self._pixelcount_factory(self)

@property
def fast_axis(self):
Expand All @@ -291,15 +301,19 @@ def timestamps(self) -> np.ndarray:
def pixelsize_um(self):
"""Returns a `List` of axes dimensions in um. The length of the list corresponds to the
number of scan axes."""
return [axes["pixel size (nm)"] / 1000 for axes in self._ordered_axes()]
return self._pixelsize_factory(self)

@property
def size_um(self):
"""Returns a `List` of scan sizes in um along axes. The length of the list corresponds to
the number of scan axes."""
return [
axes["pixel size (nm)"] * axes["num of pixels"] / 1000 for axes in self._ordered_axes()
]
return list(
map(
lambda pixel_size, num_pixels: pixel_size * num_pixels,
self.pixelsize_um,
self._num_pixels,
)
)

@property
@deprecated(
Expand Down
47 changes: 37 additions & 10 deletions lumicks/pylake/kymo.py
Expand Up @@ -261,39 +261,66 @@ def plot_with_time_histogram(self, color_channel, pixels_per_bin=1, hist_ratio=0
ax_hist.set_ylabel("counts")
ax_hist.set_title(self.name)

def downsampled_by(self, time_factor, reduce=np.sum):
"""Return a copy of this Kymograph which is downsampled by `time_factor`
def downsampled_by(
self, time_factor=1, position_factor=1, reduce=np.sum, reduce_timestamps=np.mean
):
"""Return a copy of this Kymograph which is downsampled by `time_factor` in time and
`position_factor` in space.
Parameters
----------
time_factor : int
The number of pixels that will be averaged in time.
The number of pixels that will be averaged in time (default: 1).
position_factor : int
The number of pixels that will be averaged in space (default: 1).
reduce : callable
The `numpy` function which is going to reduce multiple pixels into one.
The default is `np.sum`.
reduce_timestamps : callable
The `numpy` function that is going to reduce timestamps into one.
The default is `np.mean`.
"""
result = Kymo(self.name, self.file, self.start, self.stop, self._json)

def image_factory(_, channel):
data = self._image(channel)
return block_reduce(data, (1, time_factor), func=reduce)[
:, : data.shape[1] // time_factor
return block_reduce(data, (position_factor, time_factor), func=reduce)[
: data.shape[0] // position_factor, : data.shape[1] // time_factor
]

def timestamp_factory(_):
def timestamp_factory_ill_defined(_):
raise AttributeError(
"Per-pixel timestamps are no longer available after downsampling a kymograph since "
"they are not well defined (the downsampling occurs over a non contiguous time "
"window). Line timestamps are still available however. See: "
"Per-pixel timestamps are no longer available after downsampling a kymograph in "
"time since they are not well defined (the downsampling occurs over a "
"non-contiguous time window). Line timestamps are still available, however. See: "
"`Kymo.line_time_seconds`."
)

def timestamp_factory(_):
return block_reduce(self.timestamps, (position_factor, 1), func=reduce_timestamps)[
: self.timestamps.shape[0] // position_factor, :
]

def line_time_factory(_):
return self.line_time_seconds * time_factor

def pixelsize_factory(_):
pixelsizes = self.pixelsize_um
pixelsizes[0] = pixelsizes[0] * position_factor
return pixelsizes

def pixelcount_factory(_):
num_pixels = self._num_pixels
num_pixels[0] = num_pixels[0] // position_factor
return num_pixels

result._image_factory = image_factory
result._timestamp_factory = timestamp_factory
result._timestamp_factory = (
timestamp_factory if time_factor == 1 else timestamp_factory_ill_defined
)
result._line_time_factory = line_time_factory
result._pixelsize_factory = pixelsize_factory
result._pixelcount_factory = pixelcount_factory
return result


Expand Down
105 changes: 103 additions & 2 deletions lumicks/pylake/tests/test_kymo.py
Expand Up @@ -242,7 +242,7 @@ def test_downsampled_kymo():
"Mock", image, pixel_size_nm=1, start=100, dt=7, samples_per_pixel=5, line_padding=2
)

kymo_ds = kymo.downsampled_by(2)
kymo_ds = kymo.downsampled_by(time_factor=2)
ds = np.array(
[
[12, 12, 6],
Expand All @@ -266,7 +266,108 @@ def test_downsampled_kymo():
kymo_ds.timestamps

# Verify that we can pass a different reduce function
assert np.allclose(kymo.downsampled_by(2, reduce=np.mean).red_image, ds / 2)
assert np.allclose(kymo.downsampled_by(time_factor=2, reduce=np.mean).red_image, ds / 2)


def test_downsampled_kymo_position():
"""Test downsampling over the spatial axis"""
image = np.array(
[
[0, 12, 0, 12, 0, 6, 0],
[0, 0, 0, 0, 0, 6, 0],
[12, 0, 0, 0, 12, 6, 0],
[0, 12, 12, 12, 0, 6, 0],
[0, 12, 12, 12, 0, 6, 0],
],
dtype=np.uint8
)

kymo = generate_kymo(
"Mock", image, pixel_size_nm=1, start=100, dt=5, samples_per_pixel=5, line_padding=2
)

kymo_ds = kymo.downsampled_by(position_factor=2)
ds = np.array([[0, 12, 0, 12, 0, 12, 0], [12, 12, 12, 12, 12, 12, 0]], dtype=np.uint8)
ds_ts = np.array([[132.5, 277.5, 422.5, 567.5, 712.5, 857.5, 1002.5],
[182.5, 327.5, 472.5, 617.5, 762.5, 907.5, 1052.5]])

assert kymo_ds.name == "Mock"
assert np.allclose(kymo_ds.red_image, ds)
assert np.allclose(kymo_ds.timestamps, ds_ts)
assert np.allclose(kymo_ds.start, 100)
assert np.allclose(kymo_ds.pixelsize_um, 2 / 1000)
assert np.allclose(kymo_ds.line_time_seconds, kymo.line_time_seconds)

# We lost one line while downsampling
assert np.allclose(kymo_ds.size_um[0], kymo.size_um[0] - kymo.pixelsize_um[0])

# Verify that we can pass a different reduce function
alt_ds = kymo.downsampled_by(position_factor=2, reduce=np.mean, reduce_timestamps=np.sum)
assert np.allclose(alt_ds.red_image, ds / 2)
assert np.allclose(alt_ds.timestamps, ds_ts * 2)


def test_downsampled_kymo_both_axes():
image = np.array(
[
[0, 12, 0, 12, 0, 6, 0],
[0, 0, 0, 0, 0, 6, 0],
[12, 0, 0, 0, 12, 6, 0],
[0, 12, 12, 12, 0, 6, 0],
[0, 12, 12, 12, 0, 6, 0],
],
dtype=np.uint8
)

kymo = generate_kymo(
"Mock", image, pixel_size_nm=1, start=100, dt=5, samples_per_pixel=5, line_padding=2
)
ds = np.array([[12, 12, 12], [24, 24, 24]], dtype=np.uint8)

downsampled_kymos = [
kymo.downsampled_by(time_factor=2, position_factor=2),
# Test whether sequential downsampling works out correctly as well
kymo.downsampled_by(position_factor=2).downsampled_by(time_factor=2),
kymo.downsampled_by(time_factor=2).downsampled_by(position_factor=2)
]

for kymo_ds in downsampled_kymos:
assert kymo_ds.name == "Mock"
assert np.allclose(kymo_ds.red_image, ds)
assert np.allclose(kymo_ds.start, 100)
assert np.allclose(kymo_ds.pixelsize_um, 2 / 1000)
assert np.allclose(kymo_ds.line_time_seconds, 2 * 5 * (5 * 5 + 2 + 2) / 1e9)
with pytest.raises(
AttributeError,
match=re.escape("Per-pixel timestamps are no longer available after downsampling"),
):
kymo_ds.timestamps


def test_side_no_side_effects_downsampling():
"""Test whether downsampling doesn't have side effects on the original kymo"""
image = np.array(
[
[0, 12, 0, 12, 0, 6, 0],
[0, 0, 0, 0, 0, 6, 0],
[12, 0, 0, 0, 12, 6, 0],
[0, 12, 12, 12, 0, 6, 0],
[0, 12, 12, 12, 0, 6, 0],
],
dtype=np.uint8
)

kymo = generate_kymo(
"Mock", image, pixel_size_nm=1, start=100, dt=5, samples_per_pixel=5, line_padding=2
)
timestamps = kymo.timestamps.copy()
downsampled_kymos = kymo.downsampled_by(time_factor=2, position_factor=2)

assert np.allclose(kymo.red_image, image)
assert np.allclose(kymo.start, 100)
assert np.allclose(kymo.pixelsize_um, 1 / 1000)
assert np.allclose(kymo.line_time_seconds, 5 * (5 * 5 + 2 + 2) / 1e9)
assert np.allclose(kymo.timestamps, timestamps)


def test_calibrated_channels():
Expand Down

0 comments on commit b180068

Please sign in to comment.