From bfcfbf9387f7e7bc762d415fe239ba4556008cac Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Fri, 5 Jan 2024 14:00:00 +0000 Subject: [PATCH 1/5] copy binning bugfix from brainmapper --- brainglobe_utils/image/binning.py | 2 +- brainglobe_utils/image/heatmap.py | 4 ++++ tests/tests/test_image/test_binning.py | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 brainglobe_utils/image/heatmap.py diff --git a/brainglobe_utils/image/binning.py b/brainglobe_utils/image/binning.py index 383b2ac..ba015fd 100644 --- a/brainglobe_utils/image/binning.py +++ b/brainglobe_utils/image/binning.py @@ -11,5 +11,5 @@ def get_bins(image_size, bin_sizes): """ bins = [] for dim in range(0, len(image_size)): - bins.append(np.arange(0, image_size[dim], bin_sizes[dim])) + bins.append(np.arange(0, image_size[dim] + 1, bin_sizes[dim])) return bins diff --git a/brainglobe_utils/image/heatmap.py b/brainglobe_utils/image/heatmap.py new file mode 100644 index 0000000..0ded6fe --- /dev/null +++ b/brainglobe_utils/image/heatmap.py @@ -0,0 +1,4 @@ +def create_points_heatmap( + points, + atlas +): diff --git a/tests/tests/test_image/test_binning.py b/tests/tests/test_image/test_binning.py index c5896ec..aad5442 100644 --- a/tests/tests/test_image/test_binning.py +++ b/tests/tests/test_image/test_binning.py @@ -7,9 +7,9 @@ def test_get_bins(): image_size = (1000, 20, 125, 725) bin_sizes = (500, 2, 25, 100) - dim0_bins = np.array((0, 500)) - dim1_bins = np.array((0, 2, 4, 6, 8, 10, 12, 14, 16, 18)) - dim2_bins = np.array((0, 25, 50, 75, 100)) + dim0_bins = np.array((0, 500, 1000)) + dim1_bins = np.array((0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20)) + dim2_bins = np.array((0, 25, 50, 75, 100, 125)) dim3_bins = np.array((0, 100, 200, 300, 400, 500, 600, 700)) bins = binning.get_bins(image_size, bin_sizes) From 62daf11dc4907d4107ed41e64c04061b85f6896e Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Fri, 5 Jan 2024 16:25:49 +0000 Subject: [PATCH 2/5] Add heatmap functionality from brainmapper --- brainglobe_utils/image/heatmap.py | 113 ++++++++++++++++++++++++- tests/data/image/heatmap.tif | Bin 0 -> 3750 bytes tests/tests/test_image/test_heatmap.py | 41 +++++++++ 3 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 tests/data/image/heatmap.tif create mode 100644 tests/tests/test_image/test_heatmap.py diff --git a/brainglobe_utils/image/heatmap.py b/brainglobe_utils/image/heatmap.py index 0ded6fe..9c306b5 100644 --- a/brainglobe_utils/image/heatmap.py +++ b/brainglobe_utils/image/heatmap.py @@ -1,4 +1,109 @@ -def create_points_heatmap( - points, - atlas -): +from pathlib import Path +from typing import Tuple, Union + +import imio +import numpy as np +from scipy.ndimage import zoom +from skimage.filters import gaussian + +from brainglobe_utils.general.system import ensure_directory_exists +from brainglobe_utils.image.binning import get_bins +from brainglobe_utils.image.masking import mask_image_threshold +from brainglobe_utils.image.scale import scale_and_convert_to_16_bits + + +def rescale_array(source_array, target_array, order=1): + """ + Rescale a source array to the size of a target array. + + Parameters + ---------- + source_array : np.ndarray + The array to be rescaled. + target_array : np.ndarray + The array whose size will be matched. + order : int (default=1) + Returns + ------- + np.ndarray + The rescaled source array. + """ + + # Compute the zoom factors for each dimension + zoom_factors = [ + t / s for s, t in zip(source_array.shape, target_array.shape) + ] + + # Use scipy's zoom function to rescale the array + rescaled_array = zoom(source_array, zoom_factors, order=order) + + return rescaled_array + + +def heatmap_from_points( + points: np.ndarray, + image_resolution: float, + image_shape: Tuple[int, int, int], + bin_sizes: Tuple[int, int, int] = (1, 1, 1), + output_filename: Union[str, Path, None] = None, + smoothing: Union[float, None] = None, + mask_image: Union[np.ndarray, None] = None, +) -> np.ndarray: + """ + Generate a heatmap from a set of points and a reference image that + defines the shape and resolution of the heatmap. + + Parameters + ---------- + points : np.ndarray + Data points to be used for heatmap generation. + These may be cell positions, e.g. from cellfinder. + image_resolution : float + Resolution of the image to be generated. + image_shape : Tuple[int, int, int] + The shape of the downsampled heatmap as a tuple of integers + bin_sizes : Tuple[int, int, int], optional + The bin sizes for each dimension of the heatmap (default is (1, 1, 1)). + output_filename : Union[str, pathlib.Path, None] + The filename or path where the heatmap image will be saved. + Can be a string or a pathlib.Path object. If None, the heatmap will + not be saved + smoothing : float, optional + Smoothing factor to be applied to the heatmap. Expressed in "real" + units (e.g. microns). This will be converted to voxel units based on + the image_resolution. + If None, no smoothing is applied (default is None). + mask_image : Union[np.ndarray, None], optional + An optional mask image (as a NumPy array) to apply to the heatmap. + This could be e.g. a registered atlas image (so points outside the + brain are masked). The image will be + binarised, so this image can be binary or not. + If None, no mask is applied (default is None). + + Returns + ------- + np.ndarray + The generated heatmap as a NumPy array. + """ + + bins = get_bins(image_shape, bin_sizes) + heatmap_array, _ = np.histogramdd(points, bins=bins) + heatmap_array = heatmap_array.astype(np.uint16) + + if smoothing is not None: + smoothing = int(round(smoothing / image_resolution)) + heatmap_array = gaussian(heatmap_array, sigma=smoothing) + + if mask_image is not None: + if mask_image.shape != heatmap_array.shape: + mask_image = rescale_array(mask_image, heatmap_array) + + heatmap_array = mask_image_threshold(heatmap_array, mask_image) + + heatmap_array = scale_and_convert_to_16_bits(heatmap_array) + + if output_filename is not None: + ensure_directory_exists(Path(output_filename).parent) + imio.to_tiff(heatmap_array, output_filename) + + return heatmap_array diff --git a/tests/data/image/heatmap.tif b/tests/data/image/heatmap.tif new file mode 100644 index 0000000000000000000000000000000000000000..31a8d9e385dffde20afe01d505170cb8a5d871bd GIT binary patch literal 3750 zcmcJSe@I(b6vy9>ydMT_D_pLwB2;mFzEgXQo8$W|$pb49y*n~gP6n4$+9#0d(5xIym-^_p%z0@GeYbe z`Lxse(zL8iGI-)&uA9~imOu4VEY^;GJW~10vch+0ur^ZJAhemyyp7PFS=!7qMbEVo zv37+w^Ut*%8W*ughsLInSQm4gV1fJ$G22f+u!l-AdJnP3Sl9VuBb6!R4u|>9c3*AT ztX(TH^H_W3ebnj_r-jZUtUYRdaD391EuSVdmpEx#taKdMCe}_1*>cB$j6tWvYdedk zi?(^4?jYd{RS26!lt4`2Kar#mmIJIcH5nT>SQMn0Vfluv2ka2wS|wzEC_vu>tuRVqRG| zeKi!>-}>WW%vb#jpT0Ymlj0)Q`1EhLGD*>&t;gEku}pF+c0(#94r@*l{Wqk}mW#D` zUR=!9mpU)j9{m7o&>4i{8Hx?a&k*xZo2GR-y%kHj@xG?@g-!pGJvr{h8lCnpx$jgb zW_?)OirJIZiT;7ncC4k`%rQ%w|^iP<$AiCu?FQjC~rV#5Q=9gHXuJk% zB6AM&RVRe`w#B(?au92?zFd?!CkF>TScCF2lcY&U?Ocx>9O{yrum;r>C_h8F4$2$Q8HC~)iVeum5c8VY-8|@#oBsI-bqPeD@xFb` zUhnb{)uH$4T>_{s?DZ}YWRtziH|Sm|YvfRe@&{Bw)uKFz+CGv{vDKmc64hn3D1R;( zWfiK}YEcGJ)hvuJH&moFDzCR7ad``MU26IAcEKoPTJREvUOx%a^+!c?Egn Pdh|`E8s+~8l^FdGKaZe~ literal 0 HcmV?d00001 diff --git a/tests/tests/test_image/test_heatmap.py b/tests/tests/test_image/test_heatmap.py new file mode 100644 index 0000000..e8c8c36 --- /dev/null +++ b/tests/tests/test_image/test_heatmap.py @@ -0,0 +1,41 @@ +import numpy as np +import importlib.resources as pkg_resources + +import pytest +from tifffile import imread +from brainglobe_utils.image.heatmap import rescale_array, heatmap_from_points + +points = np.array([[5, 5, 5], [10, 10, 10], [15, 15, 15]]) +heatmap_validate_path = pkg_resources.path("tests.data.image", "heatmap.tif") + +@pytest.fixture +def mask_array(): + """ + A simple array of size (20,20,20) where the left side + is 0 and the right side is 1 + """ + array = np.zeros((20, 20, 20)) + array[:, :, 10:] = 1 + return array + + +def test_rescale_array(mask_array): + small_array = np.zeros((10, 10, 10)) + resized_array = rescale_array(mask_array, small_array) + assert resized_array.shape == small_array.shape + +def test_heatmap_from_points(tmp_path, mask_array): + output_filename = tmp_path / "test_heatmap.tif" + + heatmap_test = heatmap_from_points(points, + 2, + (20, 20, 20), + bin_sizes=(2, 2, 2), + output_filename=output_filename, + smoothing=5, + mask_image=mask_array) + heatmap_validate = imread(heatmap_validate_path) + assert np.array_equal(heatmap_validate, heatmap_test) + + heatmap_file = imread(output_filename) + assert np.array_equal(heatmap_validate, heatmap_file) \ No newline at end of file From 629bfd74c3d621c7a4b55769b6ed4f7fe658c406 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:27:13 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/tests/test_image/test_heatmap.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/tests/test_image/test_heatmap.py b/tests/tests/test_image/test_heatmap.py index e8c8c36..6801579 100644 --- a/tests/tests/test_image/test_heatmap.py +++ b/tests/tests/test_image/test_heatmap.py @@ -1,13 +1,15 @@ -import numpy as np import importlib.resources as pkg_resources +import numpy as np import pytest from tifffile import imread -from brainglobe_utils.image.heatmap import rescale_array, heatmap_from_points + +from brainglobe_utils.image.heatmap import heatmap_from_points, rescale_array points = np.array([[5, 5, 5], [10, 10, 10], [15, 15, 15]]) heatmap_validate_path = pkg_resources.path("tests.data.image", "heatmap.tif") + @pytest.fixture def mask_array(): """ @@ -24,18 +26,21 @@ def test_rescale_array(mask_array): resized_array = rescale_array(mask_array, small_array) assert resized_array.shape == small_array.shape + def test_heatmap_from_points(tmp_path, mask_array): output_filename = tmp_path / "test_heatmap.tif" - heatmap_test = heatmap_from_points(points, - 2, - (20, 20, 20), - bin_sizes=(2, 2, 2), - output_filename=output_filename, - smoothing=5, - mask_image=mask_array) + heatmap_test = heatmap_from_points( + points, + 2, + (20, 20, 20), + bin_sizes=(2, 2, 2), + output_filename=output_filename, + smoothing=5, + mask_image=mask_array, + ) heatmap_validate = imread(heatmap_validate_path) assert np.array_equal(heatmap_validate, heatmap_test) heatmap_file = imread(output_filename) - assert np.array_equal(heatmap_validate, heatmap_file) \ No newline at end of file + assert np.array_equal(heatmap_validate, heatmap_file) From d594fe8a1944f8b1cdef5ad366406f92f4472892 Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Fri, 5 Jan 2024 16:34:42 +0000 Subject: [PATCH 4/5] update dependencies --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index af7f542..15a1583 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,9 @@ dependencies = [ "PyYAML", "scipy", "numpy", - "slurmio" + "slurmio", + "tifffile", + "imio" ] license = {text = "MIT"} From ac20a66465eb5445a3c45a45d82af8ec288fa472 Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Fri, 5 Jan 2024 16:59:30 +0000 Subject: [PATCH 5/5] Update path to test data --- tests/tests/test_image/test_heatmap.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/tests/test_image/test_heatmap.py b/tests/tests/test_image/test_heatmap.py index 6801579..8b91bf1 100644 --- a/tests/tests/test_image/test_heatmap.py +++ b/tests/tests/test_image/test_heatmap.py @@ -1,4 +1,4 @@ -import importlib.resources as pkg_resources +from pathlib import Path import numpy as np import pytest @@ -6,8 +6,10 @@ from brainglobe_utils.image.heatmap import heatmap_from_points, rescale_array +data_dir = Path("tests", "data") +heatmap_validate_path = data_dir / "image" / "heatmap.tif" + points = np.array([[5, 5, 5], [10, 10, 10], [15, 15, 15]]) -heatmap_validate_path = pkg_resources.path("tests.data.image", "heatmap.tif") @pytest.fixture