Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement dask-downsampling #21

Merged
merged 4 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions brainglobe_template_builder/napari/_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
implement multiple readers or even other plugin contributions. see:
https://napari.org/stable/plugins/guides.html?#readers
"""

from pathlib import Path

from brainglobe_utils.IO.image.load import load_any
Expand Down
59 changes: 59 additions & 0 deletions brainglobe_template_builder/preproc/transform_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import dask.array as da
import numpy as np
from scipy.ndimage import affine_transform
from skimage import transform


def get_rotation_from_vectors(vec1: np.ndarray, vec2: np.ndarray):
Expand Down Expand Up @@ -71,3 +73,60 @@ def apply_transform(
# Preserve original data range and type
transformed = np.clip(transformed, data.min(), data.max())
return transformed.astype(data.dtype)


def downsample_anisotropic_image_stack(
stack: da.Array, xy_downsampling: int, z_downsampling: int
) -> np.ndarray:
alessandrofelder marked this conversation as resolved.
Show resolved Hide resolved
"""

Lazily downsamples a dask array first along axis 1,2 and then along axis 0,
using a local mean of the pixels. The (smaller) array is returned
in memory (numpy) form at the end.
alessandrofelder marked this conversation as resolved.
Show resolved Hide resolved

This setup is typical for certain types of microscopy,
where z-resolution is lower than x-y-resolution.

The input dask array must be chunked by x-y slice,
alessandrofelder marked this conversation as resolved.
Show resolved Hide resolved

Parameters:
----------
stack : da.Array
The input dask array representing the image stack.
xy_downsampling : int
The downsampling factor for the x and y axes.
z_downsampling : int
alessandrofelder marked this conversation as resolved.
Show resolved Hide resolved
The downsampling factor for the z axis.
Returns:
-------
np.ndarray
The computed downsampled (numpy) array.
Raises:
------
AssertionError
If the array is not chunked slice-wise on axis 0.
"""
# check we have expected slice chunks
assert np.all(
np.array(stack.chunks[0]) == 1
), f"Array not chunked slice-wise! Chunks on axis 0 are {stack.chunks[0]}"

# we have xy slices as chunks, so apply downscaling in xy first
downsampled_xy = stack.map_blocks(
transform.downscale_local_mean,
(1, xy_downsampling, xy_downsampling),
dtype=np.float64,
)

# rechunk so we can map_blocks along z
downsampled_xy = downsampled_xy.rechunk(
{0: downsampled_xy.shape[0], 1: -1, 2: -1}
)

# downsample in z
downsampled_z = downsampled_xy.map_blocks(
transform.downscale_local_mean,
(z_downsampling, 1, 1),
dtype=np.float64,
)
return downsampled_z.compute()
50 changes: 50 additions & 0 deletions tests/test_unit/test_transform_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import dask.array as da
import numpy as np
import pytest
from skimage import transform

from brainglobe_template_builder.preproc.transform_utils import (
downsample_anisotropic_image_stack,
)


@pytest.fixture()
def stack():
"""Create a dask array representing an image stack"""
data = np.random.rand(10, 100, 100) # Random image stack
alessandrofelder marked this conversation as resolved.
Show resolved Hide resolved
return da.from_array(data, chunks=(1, 100, 100))


@pytest.fixture()
def not_slicewise_stack():
"""Create a dask array representing an image stack"""
data = np.random.rand(10, 100, 100) # Random image stack
return da.from_array(data, chunks=(2, 100, 100))


def test_downsample_anisotropic_image_stack(stack):
"""Test that downsampling with dask gives same as without."""
xy_downsampling = 20
z_downsampling = 2
alessandrofelder marked this conversation as resolved.
Show resolved Hide resolved

downsampled_stack = downsample_anisotropic_image_stack(
stack, xy_downsampling, z_downsampling
)

assert downsampled_stack.shape == (5, 5, 5)

expected = transform.downscale_local_mean(
stack.compute(), (1, xy_downsampling, xy_downsampling)
)
expected = transform.downscale_local_mean(expected, (z_downsampling, 1, 1))
assert np.all(
downsampled_stack == expected
), "dask downsampling does not match expected skimage result"


def test_downsample_anisotropic_image_stack_raises(not_slicewise_stack):
with pytest.raises(AssertionError) as e:
downsample_anisotropic_image_stack(
not_slicewise_stack, xy_downsampling=20, z_downsampling=2
)
assert e.match("not chunked slice-wise!")
alessandrofelder marked this conversation as resolved.
Show resolved Hide resolved
Loading