From 75908fea4fa5bc0545305a2aad375f0d0c430228 Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <k.meechan@ucl.ac.uk> Date: Thu, 7 Mar 2024 15:22:23 +0000 Subject: [PATCH 1/9] add docstrings for tests and update to use tmp_path --- tests/tests/test_image_io.py | 67 +++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/tests/tests/test_image_io.py b/tests/tests/test_image_io.py index 17f8438..d8cf72b 100644 --- a/tests/tests/test_image_io.py +++ b/tests/tests/test_image_io.py @@ -1,5 +1,3 @@ -import os - import numpy as np import pytest from tifffile import tifffile @@ -9,55 +7,70 @@ @pytest.fixture() def layer(): + """Create a 4x4 array of 32-bit integers""" return np.tile(np.array([1, 2, 3, 4], dtype=np.int32), (4, 1)) @pytest.fixture() def start_array(layer): + """Create a 4x4x4 array of 32-bit integers""" volume = np.dstack((layer, 2 * layer, 3 * layer, 4 * layer)) return volume -def test_tiff_io(tmpdir, layer): - folder = str(tmpdir) - dest_path = os.path.join(folder, "layer.tiff") +def test_tiff_io(tmp_path, layer): + """ + Test that a 2D tiff can be written and read correctly with tifffile + """ + dest_path = tmp_path / "layer.tiff" tifffile.imwrite(dest_path, layer) reloaded = tifffile.imread(dest_path) assert (reloaded == layer).all() -def test_to_tiffs(tmpdir, start_array): - folder = str(tmpdir) - save.to_tiffs(start_array, os.path.join(folder, "start_array")) - reloaded_array = load.load_from_folder(folder, 1, 1, 1) +def test_to_tiffs(tmp_path, start_array): + """ + Test that a 3D image can be written and read correctly as a sequence + of 2D tiffs + """ + save.to_tiffs(start_array, str(tmp_path / "start_array")) + reloaded_array = load.load_from_folder(str(tmp_path), 1, 1, 1) assert (reloaded_array == start_array).all() -def test_load_img_sequence(tmpdir, start_array): - folder = str(tmpdir.mkdir("sub")) - save.to_tiffs(start_array, os.path.join(folder, "start_array")) - img_sequence_file = tmpdir.join("imgs_file.txt") - img_sequence_file.write( - "\n".join( - [ - os.path.join(folder, fname) - for fname in sorted(os.listdir(folder)) - ] - ) +def test_load_img_sequence(tmp_path, start_array): + """ + Test that a tiff sequence can be loaded from a text file containing an + ordered list of the tiff file paths (one per line) + """ + # Write tiff sequence to sub-folder + folder = tmp_path / "sub" + folder.mkdir() + save.to_tiffs(start_array, str(folder / "start_array")) + + # Write txt file containing all tiff file paths (one per line) + img_sequence_file = tmp_path / "imgs_file.txt" + img_sequence_file.write_text( + "\n".join([str(folder / fname) for fname in sorted(folder.iterdir())]) ) + + # Load image from paths in text file reloaded_array = load.load_img_sequence(str(img_sequence_file), 1, 1, 1) assert (reloaded_array == start_array).all() -def test_to_nii(tmpdir, start_array): # Also tests load_nii - folder = str(tmpdir) - nii_path = os.path.join(folder, "test_array.nii") +def test_to_nii(tmp_path, start_array): + """ + Test that a 3D image can be written and read correctly as nii + """ + nii_path = str(tmp_path / "test_array.nii") save.to_nii(start_array, nii_path) assert (load.load_nii(nii_path).get_fdata() == start_array).all() def test_scale_z(start_array): - assert ( - utils.scale_z(start_array, 0.5).shape[0] == start_array.shape[-1] / 2 - ) - assert utils.scale_z(start_array, 2).shape[0] == start_array.shape[-1] * 2 + """ + Test that a 3D image can be scaled in z by float and integer values + """ + assert utils.scale_z(start_array, 0.5).shape[0] == start_array.shape[0] / 2 + assert utils.scale_z(start_array, 2).shape[0] == start_array.shape[0] * 2 From fd64f9ca7c60eb59735d9d8b7322096db1a6d414 Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <k.meechan@ucl.ac.uk> Date: Thu, 7 Mar 2024 16:06:35 +0000 Subject: [PATCH 2/9] update docstrings and add nrrd test --- brainglobe_utils/image_io/load.py | 2 +- brainglobe_utils/image_io/save.py | 14 +++++++------- tests/tests/test_image_io.py | 11 ++++++++++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/brainglobe_utils/image_io/load.py b/brainglobe_utils/image_io/load.py index 2999635..2711467 100644 --- a/brainglobe_utils/image_io/load.py +++ b/brainglobe_utils/image_io/load.py @@ -166,7 +166,7 @@ def load_img_stack( Parameters ---------- - stack_path : str + stack_path : str or Path The path of the image to be loaded. x_scaling_factor : float diff --git a/brainglobe_utils/image_io/save.py b/brainglobe_utils/image_io/save.py index 6c0e4b0..c048418 100644 --- a/brainglobe_utils/image_io/save.py +++ b/brainglobe_utils/image_io/save.py @@ -20,8 +20,8 @@ def to_nii(img, dest_path, scale=None, affine_transform=None): img : nifty image object or np.ndarray A nifty image object or numpy array representing a brain. - dest_path : str - The path where to save the brain. + dest_path : str or Path + The file path where to save the brain. scale : tuple of floats, optional A tuple of floats to indicate the 'zooms' of the nifty image. @@ -49,8 +49,8 @@ def to_tiff(img_volume, dest_path): img_volume : np.ndarray The image to be saved. - dest_path : str - Where to save the tiff stack. + dest_path : str or Path + The file path where to save the tiff stack. """ dest_path = str(dest_path) tifffile.imwrite(dest_path, img_volume) @@ -60,7 +60,7 @@ def to_tiffs(img_volume, path_prefix, path_suffix="", extension=".tif"): """ Save the image volume (numpy array) as a sequence of tiff planes. Each plane will have a filepath of the following format: - pathprefix_zeroPaddedIndex_suffix.tif + {path_prefix}_{zeroPaddedIndex}{path_suffix}{extension} Parameters ---------- @@ -95,8 +95,8 @@ def to_nrrd(img_volume, dest_path): img_volume : np.ndarray The image to be saved. - dest_path : str - Where to save the nrrd image. + dest_path : str or Path + The file path where to save the nrrd image. """ dest_path = str(dest_path) nrrd.write(dest_path, img_volume) diff --git a/tests/tests/test_image_io.py b/tests/tests/test_image_io.py index d8cf72b..a8f78b0 100644 --- a/tests/tests/test_image_io.py +++ b/tests/tests/test_image_io.py @@ -64,10 +64,19 @@ def test_to_nii(tmp_path, start_array): Test that a 3D image can be written and read correctly as nii """ nii_path = str(tmp_path / "test_array.nii") - save.to_nii(start_array, nii_path) + save.to_nii(start_array, nii_path, scale=(1, 1, 1)) assert (load.load_nii(nii_path).get_fdata() == start_array).all() +def test_to_nrrd(tmp_path, start_array): + """ + Test that a 3D image can be written and read correctly as nrrd + """ + nrrd_path = str(tmp_path / "test_array.nrrd") + save.to_nrrd(start_array, nrrd_path) + assert (load.load_nrrd(nrrd_path) == start_array).all() + + def test_scale_z(start_array): """ Test that a 3D image can be scaled in z by float and integer values From dbd9962579b262ab09cdc6884f68ce4c778d61cd Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <k.meechan@ucl.ac.uk> Date: Mon, 11 Mar 2024 12:00:57 +0000 Subject: [PATCH 3/9] test load_any and 3D tiff --- brainglobe_utils/image_io/load.py | 6 +- tests/tests/test_image_io.py | 104 ++++++++++++++++++++++++++---- 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/brainglobe_utils/image_io/load.py b/brainglobe_utils/image_io/load.py index 2711467..a1c4284 100644 --- a/brainglobe_utils/image_io/load.py +++ b/brainglobe_utils/image_io/load.py @@ -43,9 +43,9 @@ def load_any( Parameters ---------- - src_path : str - Can be the path of a nifty file, tiff file, tiff files folder, or text - file containing a list of paths. + src_path : str or Path + Can be the path of a nifty file, nrrd file, tiff file, tiff files + folder, or text file containing a list of paths. x_scaling_factor : float, optional The scaling of the brain along the x dimension (applied on loading diff --git a/tests/tests/test_image_io.py b/tests/tests/test_image_io.py index a8f78b0..4bc88e1 100644 --- a/tests/tests/test_image_io.py +++ b/tests/tests/test_image_io.py @@ -18,7 +18,51 @@ def start_array(layer): return volume -def test_tiff_io(tmp_path, layer): +def write_tiff_sequence_with_txt_file(txt_path, image_array): + """ + Write an image array to a series of tiffs, and write a text file + containing all the tiff file paths in order (one per line). + + The tiff sequence will be saved to a sub-folder inside the same folder + as the text file. + """ + directory = txt_path.parent + + # Write tiff sequence to sub-folder + sub_dir = directory / "sub" + sub_dir.mkdir() + save.to_tiffs(image_array, str(sub_dir / "image_array")) + + # Write txt file containing all tiff file paths (one per line) + txt_path.write_text( + "\n".join( + [str(sub_dir / fname) for fname in sorted(sub_dir.iterdir())] + ) + ) + + +def save_any(file_path, image_array): + """ + Save image_array to given file path, using the save function matching + its file extension + """ + if file_path.is_dir(): + save.to_tiffs(image_array, str(file_path / "image_array")) + + elif file_path.suffix == ".txt": + write_tiff_sequence_with_txt_file(file_path, image_array) + + elif file_path.suffix == ".tif" or file_path.suffix == ".tiff": + save.to_tiff(image_array, file_path) + + elif file_path.suffix == ".nrrd": + save.to_nrrd(image_array, file_path) + + elif file_path.suffix == ".nii": + save.to_nii(image_array, file_path) + + +def test_single_2d_tifffile_io(tmp_path, layer): """ Test that a 2D tiff can be written and read correctly with tifffile """ @@ -28,7 +72,18 @@ def test_tiff_io(tmp_path, layer): assert (reloaded == layer).all() -def test_to_tiffs(tmp_path, start_array): +def test_single_3d_tiff_io(tmp_path, start_array): + """ + Test that a 3D tiff can be written and read correctly with image_io + """ + dest_path = tmp_path / "layer.tiff" + save.to_tiff(start_array, dest_path) + + reloaded = load.load_img_stack(dest_path, 1, 1, 1) + assert (reloaded == layer).all() + + +def test_tiff_sequence_io(tmp_path, start_array): """ Test that a 3D image can be written and read correctly as a sequence of 2D tiffs @@ -38,28 +93,20 @@ def test_to_tiffs(tmp_path, start_array): assert (reloaded_array == start_array).all() -def test_load_img_sequence(tmp_path, start_array): +def test_load_img_sequence_from_txt(tmp_path, start_array): """ Test that a tiff sequence can be loaded from a text file containing an ordered list of the tiff file paths (one per line) """ - # Write tiff sequence to sub-folder - folder = tmp_path / "sub" - folder.mkdir() - save.to_tiffs(start_array, str(folder / "start_array")) - - # Write txt file containing all tiff file paths (one per line) img_sequence_file = tmp_path / "imgs_file.txt" - img_sequence_file.write_text( - "\n".join([str(folder / fname) for fname in sorted(folder.iterdir())]) - ) + write_tiff_sequence_with_txt_file(img_sequence_file, start_array) # Load image from paths in text file reloaded_array = load.load_img_sequence(str(img_sequence_file), 1, 1, 1) assert (reloaded_array == start_array).all() -def test_to_nii(tmp_path, start_array): +def test_nii_io(tmp_path, start_array): """ Test that a 3D image can be written and read correctly as nii """ @@ -68,7 +115,7 @@ def test_to_nii(tmp_path, start_array): assert (load.load_nii(nii_path).get_fdata() == start_array).all() -def test_to_nrrd(tmp_path, start_array): +def test_nrrd_io(tmp_path, start_array): """ Test that a 3D image can be written and read correctly as nrrd """ @@ -77,6 +124,35 @@ def test_to_nrrd(tmp_path, start_array): assert (load.load_nrrd(nrrd_path) == start_array).all() +@pytest.mark.parametrize( + "file_name", + [ + "test_array.tiff", + "test_array.tif", + "test_array.txt", + "test_array.nrrd", + "test_array.nii", + pytest.param("", id="dir of tiffs"), + ], +) +def test_load_any(tmp_path, start_array, file_name): + """ + Test that load_any can read all required image file types + """ + src_path = tmp_path / file_name + save_any(src_path, start_array) + + assert (load.load_any(src_path) == start_array).all() + + +def test_load_any_error(tmp_path): + """ + Test that load_any throws an error for an unknown file extension + """ + with pytest.raises(NotImplementedError): + load.load_any(tmp_path / "test.unknown") + + def test_scale_z(start_array): """ Test that a 3D image can be scaled in z by float and integer values From 3c4f1e5db026647a07578b17cfd41ec0c7b07fdf Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <k.meechan@ucl.ac.uk> Date: Mon, 11 Mar 2024 15:34:56 +0000 Subject: [PATCH 4/9] test 3D tiff scaling --- brainglobe_utils/image_io/load.py | 3 +- tests/tests/test_image_io.py | 87 +++++++++++++++++++------------ 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/brainglobe_utils/image_io/load.py b/brainglobe_utils/image_io/load.py index a1c4284..ad6ac91 100644 --- a/brainglobe_utils/image_io/load.py +++ b/brainglobe_utils/image_io/load.py @@ -401,8 +401,7 @@ def load_image_series( n_free_cpus=2, ): """ - Load a brain from a sequence of files specified in a text file containing - an ordered list of paths. + Load a brain from a sequence of image paths. Parameters ---------- diff --git a/tests/tests/test_image_io.py b/tests/tests/test_image_io.py index 4bc88e1..282a47c 100644 --- a/tests/tests/test_image_io.py +++ b/tests/tests/test_image_io.py @@ -1,23 +1,31 @@ import numpy as np import pytest -from tifffile import tifffile from brainglobe_utils.image_io import load, save, utils @pytest.fixture() -def layer(): +def array_2d(): """Create a 4x4 array of 32-bit integers""" return np.tile(np.array([1, 2, 3, 4], dtype=np.int32), (4, 1)) @pytest.fixture() -def start_array(layer): +def array_3d(array_2d): """Create a 4x4x4 array of 32-bit integers""" - volume = np.dstack((layer, 2 * layer, 3 * layer, 4 * layer)) + volume = np.dstack((array_2d, 2 * array_2d, 3 * array_2d, 4 * array_2d)) return volume +@pytest.fixture(params=["2D", "3D"]) +def image_array(request, array_2d, array_3d): + """Create both a 2D and 3D array of 32-bit integers""" + if request.param == "2D": + return array_2d + else: + return array_3d + + def write_tiff_sequence_with_txt_file(txt_path, image_array): """ Write an image array to a series of tiffs, and write a text file @@ -62,66 +70,77 @@ def save_any(file_path, image_array): save.to_nii(image_array, file_path) -def test_single_2d_tifffile_io(tmp_path, layer): +def test_tiff_io(tmp_path, image_array): """ - Test that a 2D tiff can be written and read correctly with tifffile + Test that a 2D/3D tiff can be written and read correctly """ - dest_path = tmp_path / "layer.tiff" - tifffile.imwrite(dest_path, layer) - reloaded = tifffile.imread(dest_path) - assert (reloaded == layer).all() + dest_path = tmp_path / "image_array.tiff" + save.to_tiff(image_array, dest_path) + + reloaded = load.load_img_stack(dest_path, 1, 1, 1) + assert (reloaded == image_array).all() -def test_single_3d_tiff_io(tmp_path, start_array): +@pytest.mark.parametrize( + "x_scaling_factor, y_scaling_factor, z_scaling_factor", + [(1, 1, 1), (0.5, 0.5, 1), (0.25, 0.25, 0.25)], +) +def test_3d_tiff_scaling( + tmp_path, array_3d, x_scaling_factor, y_scaling_factor, z_scaling_factor +): """ - Test that a 3D tiff can be written and read correctly with image_io + Test that a 3D tiff is scaled correctly on loading """ - dest_path = tmp_path / "layer.tiff" - save.to_tiff(start_array, dest_path) + dest_path = tmp_path / "image_array.tiff" + save.to_tiff(array_3d, dest_path) - reloaded = load.load_img_stack(dest_path, 1, 1, 1) - assert (reloaded == layer).all() + reloaded = load.load_img_stack( + dest_path, x_scaling_factor, y_scaling_factor, z_scaling_factor + ) + assert reloaded.shape[0] == array_3d.shape[0] * z_scaling_factor + assert reloaded.shape[1] == array_3d.shape[1] * y_scaling_factor + assert reloaded.shape[2] == array_3d.shape[2] * x_scaling_factor -def test_tiff_sequence_io(tmp_path, start_array): +def test_tiff_sequence_io(tmp_path, array_3d): """ Test that a 3D image can be written and read correctly as a sequence of 2D tiffs """ - save.to_tiffs(start_array, str(tmp_path / "start_array")) + save.to_tiffs(array_3d, str(tmp_path / "image_array")) reloaded_array = load.load_from_folder(str(tmp_path), 1, 1, 1) - assert (reloaded_array == start_array).all() + assert (reloaded_array == array_3d).all() -def test_load_img_sequence_from_txt(tmp_path, start_array): +def test_load_img_sequence_from_txt(tmp_path, array_3d): """ Test that a tiff sequence can be loaded from a text file containing an ordered list of the tiff file paths (one per line) """ img_sequence_file = tmp_path / "imgs_file.txt" - write_tiff_sequence_with_txt_file(img_sequence_file, start_array) + write_tiff_sequence_with_txt_file(img_sequence_file, array_3d) # Load image from paths in text file reloaded_array = load.load_img_sequence(str(img_sequence_file), 1, 1, 1) - assert (reloaded_array == start_array).all() + assert (reloaded_array == array_3d).all() -def test_nii_io(tmp_path, start_array): +def test_nii_io(tmp_path, array_3d): """ Test that a 3D image can be written and read correctly as nii """ nii_path = str(tmp_path / "test_array.nii") - save.to_nii(start_array, nii_path, scale=(1, 1, 1)) - assert (load.load_nii(nii_path).get_fdata() == start_array).all() + save.to_nii(array_3d, nii_path, scale=(1, 1, 1)) + assert (load.load_nii(nii_path).get_fdata() == array_3d).all() -def test_nrrd_io(tmp_path, start_array): +def test_nrrd_io(tmp_path, array_3d): """ Test that a 3D image can be written and read correctly as nrrd """ nrrd_path = str(tmp_path / "test_array.nrrd") - save.to_nrrd(start_array, nrrd_path) - assert (load.load_nrrd(nrrd_path) == start_array).all() + save.to_nrrd(array_3d, nrrd_path) + assert (load.load_nrrd(nrrd_path) == array_3d).all() @pytest.mark.parametrize( @@ -135,14 +154,14 @@ def test_nrrd_io(tmp_path, start_array): pytest.param("", id="dir of tiffs"), ], ) -def test_load_any(tmp_path, start_array, file_name): +def test_load_any(tmp_path, array_3d, file_name): """ Test that load_any can read all required image file types """ src_path = tmp_path / file_name - save_any(src_path, start_array) + save_any(src_path, array_3d) - assert (load.load_any(src_path) == start_array).all() + assert (load.load_any(src_path) == array_3d).all() def test_load_any_error(tmp_path): @@ -153,9 +172,9 @@ def test_load_any_error(tmp_path): load.load_any(tmp_path / "test.unknown") -def test_scale_z(start_array): +def test_scale_z(array_3d): """ Test that a 3D image can be scaled in z by float and integer values """ - assert utils.scale_z(start_array, 0.5).shape[0] == start_array.shape[0] / 2 - assert utils.scale_z(start_array, 2).shape[0] == start_array.shape[0] * 2 + assert utils.scale_z(array_3d, 0.5).shape[0] == array_3d.shape[0] / 2 + assert utils.scale_z(array_3d, 2).shape[0] == array_3d.shape[0] * 2 From b064e33ee67d9d116f4b17083fb8a09ac9940c09 Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <k.meechan@ucl.ac.uk> Date: Mon, 11 Mar 2024 16:14:17 +0000 Subject: [PATCH 5/9] test txt sequence sorting and nii to numpy --- tests/tests/test_image_io.py | 47 ++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/tests/test_image_io.py b/tests/tests/test_image_io.py index 282a47c..0118363 100644 --- a/tests/tests/test_image_io.py +++ b/tests/tests/test_image_io.py @@ -1,3 +1,5 @@ +import random + import numpy as np import pytest @@ -13,7 +15,7 @@ def array_2d(): @pytest.fixture() def array_3d(array_2d): """Create a 4x4x4 array of 32-bit integers""" - volume = np.dstack((array_2d, 2 * array_2d, 3 * array_2d, 4 * array_2d)) + volume = np.stack((array_2d, 2 * array_2d, 3 * array_2d, 4 * array_2d)) return volume @@ -26,7 +28,7 @@ def image_array(request, array_2d, array_3d): return array_3d -def write_tiff_sequence_with_txt_file(txt_path, image_array): +def write_tiff_sequence_with_txt_file(txt_path, image_array, shuffle=False): """ Write an image array to a series of tiffs, and write a text file containing all the tiff file paths in order (one per line). @@ -42,10 +44,11 @@ def write_tiff_sequence_with_txt_file(txt_path, image_array): save.to_tiffs(image_array, str(sub_dir / "image_array")) # Write txt file containing all tiff file paths (one per line) + tiff_paths = sorted(sub_dir.iterdir()) + if shuffle: + random.Random(4).shuffle(tiff_paths) txt_path.write_text( - "\n".join( - [str(sub_dir / fname) for fname in sorted(sub_dir.iterdir())] - ) + "\n".join([str(sub_dir / fname) for fname in tiff_paths]) ) @@ -125,6 +128,29 @@ def test_load_img_sequence_from_txt(tmp_path, array_3d): assert (reloaded_array == array_3d).all() +@pytest.mark.parametrize( + "sort", + [True, False], +) +def test_sort_img_sequence_from_txt(tmp_path, array_3d, sort): + """ + Test that filepaths read from a txt file can be sorted correctly + """ + img_sequence_file = tmp_path / "imgs_file.txt" + write_tiff_sequence_with_txt_file( + img_sequence_file, array_3d, shuffle=True + ) + + # Load image from paths in text file + reloaded_array = load.load_img_sequence( + str(img_sequence_file), 1, 1, 1, sort=sort + ) + if sort: + assert (reloaded_array == array_3d).all() + else: + assert not (reloaded_array == array_3d).all() + + def test_nii_io(tmp_path, array_3d): """ Test that a 3D image can be written and read correctly as nii @@ -134,6 +160,17 @@ def test_nii_io(tmp_path, array_3d): assert (load.load_nii(nii_path).get_fdata() == array_3d).all() +def test_nii_read_to_numpy(tmp_path, array_3d): + """ + Test that conversion of loaded nii image to an in-memory numpy array works + """ + nii_path = str(tmp_path / "test_array.nii") + save.to_nii(array_3d, nii_path, scale=(1, 1, 1)) + + reloaded_array = load.load_nii(nii_path, as_array=True, as_numpy=True) + assert (reloaded_array == array_3d).all() + + def test_nrrd_io(tmp_path, array_3d): """ Test that a 3D image can be written and read correctly as nrrd From 61c7caa30524f616ecfa5ce917a77233b300b766 Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <k.meechan@ucl.ac.uk> Date: Mon, 11 Mar 2024 16:56:47 +0000 Subject: [PATCH 6/9] test tiff sequence scaling and parallel loading --- tests/tests/test_image_io.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/tests/tests/test_image_io.py b/tests/tests/test_image_io.py index 0118363..fe840d8 100644 --- a/tests/tests/test_image_io.py +++ b/tests/tests/test_image_io.py @@ -105,16 +105,45 @@ def test_3d_tiff_scaling( assert reloaded.shape[2] == array_3d.shape[2] * x_scaling_factor -def test_tiff_sequence_io(tmp_path, array_3d): +@pytest.mark.parametrize( + "load_parallel", + [ + pytest.param(True, id="parallel loading"), + pytest.param(False, id="no parallel loading"), + ], +) +def test_tiff_sequence_io(tmp_path, array_3d, load_parallel): """ Test that a 3D image can be written and read correctly as a sequence - of 2D tiffs + of 2D tiffs (with or without parallel loading) """ save.to_tiffs(array_3d, str(tmp_path / "image_array")) - reloaded_array = load.load_from_folder(str(tmp_path), 1, 1, 1) + reloaded_array = load.load_from_folder( + str(tmp_path), 1, 1, 1, load_parallel=load_parallel + ) assert (reloaded_array == array_3d).all() +@pytest.mark.parametrize( + "x_scaling_factor, y_scaling_factor, z_scaling_factor", + [(1, 1, 1), (0.5, 0.5, 1), (0.25, 0.25, 0.25)], +) +def test_tiff_sequence_scaling( + tmp_path, array_3d, x_scaling_factor, y_scaling_factor, z_scaling_factor +): + """ + Test that a tiff sequence is scaled correctly on loading + """ + save.to_tiffs(array_3d, str(tmp_path / "image_array")) + reloaded_array = load.load_from_folder( + str(tmp_path), x_scaling_factor, y_scaling_factor, z_scaling_factor + ) + + assert reloaded_array.shape[0] == array_3d.shape[0] * z_scaling_factor + assert reloaded_array.shape[1] == array_3d.shape[1] * y_scaling_factor + assert reloaded_array.shape[2] == array_3d.shape[2] * x_scaling_factor + + def test_load_img_sequence_from_txt(tmp_path, array_3d): """ Test that a tiff sequence can be loaded from a text file containing an From 799f52e0312e92fe9b94add69ad6232d67309e13 Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <k.meechan@ucl.ac.uk> Date: Tue, 12 Mar 2024 13:44:18 +0000 Subject: [PATCH 7/9] add tests for image size --- brainglobe_utils/image_io/load.py | 3 ++- tests/tests/test_image_io.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/brainglobe_utils/image_io/load.py b/brainglobe_utils/image_io/load.py index ad6ac91..1372be5 100644 --- a/brainglobe_utils/image_io/load.py +++ b/brainglobe_utils/image_io/load.py @@ -600,7 +600,8 @@ def get_size_image_from_file_paths(file_path, file_extension="tif"): Parameters ---------- file_path : str - File containing file_paths in a text file, or as a list. + Filepath of text file containing paths of all 2D files, or + filepath of a directory containing all 2D files. file_extension : str, optional Optional file extension (if a directory is passed). diff --git a/tests/tests/test_image_io.py b/tests/tests/test_image_io.py index fe840d8..7215472 100644 --- a/tests/tests/test_image_io.py +++ b/tests/tests/test_image_io.py @@ -244,3 +244,22 @@ def test_scale_z(array_3d): """ assert utils.scale_z(array_3d, 0.5).shape[0] == array_3d.shape[0] / 2 assert utils.scale_z(array_3d, 2).shape[0] == array_3d.shape[0] * 2 + + +def test_image_size_dir(tmp_path, array_3d): + save.to_tiffs(array_3d, str(tmp_path / "image_array")) + + image_shape = load.get_size_image_from_file_paths(str(tmp_path)) + assert image_shape["x"] == array_3d.shape[2] + assert image_shape["y"] == array_3d.shape[1] + assert image_shape["z"] == array_3d.shape[0] + + +def test_image_size_txt(tmp_path, array_3d): + txt_filepath = tmp_path / "image.txt" + write_tiff_sequence_with_txt_file(txt_filepath, array_3d) + + image_shape = load.get_size_image_from_file_paths(str(txt_filepath)) + assert image_shape["x"] == array_3d.shape[2] + assert image_shape["y"] == array_3d.shape[1] + assert image_shape["z"] == array_3d.shape[0] From 12da288bf215a8ecada499e32e18409a9b8a8db7 Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <k.meechan@ucl.ac.uk> Date: Wed, 13 Mar 2024 10:13:02 +0000 Subject: [PATCH 8/9] tidy up tests --- brainglobe_utils/image_io/load.py | 4 +- brainglobe_utils/image_io/save.py | 6 +-- tests/tests/test_image_io.py | 86 +++++++++++++++++++------------ 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/brainglobe_utils/image_io/load.py b/brainglobe_utils/image_io/load.py index 1372be5..8459e76 100644 --- a/brainglobe_utils/image_io/load.py +++ b/brainglobe_utils/image_io/load.py @@ -43,7 +43,7 @@ def load_any( Parameters ---------- - src_path : str or Path + src_path : str Can be the path of a nifty file, nrrd file, tiff file, tiff files folder, or text file containing a list of paths. @@ -166,7 +166,7 @@ def load_img_stack( Parameters ---------- - stack_path : str or Path + stack_path : str The path of the image to be loaded. x_scaling_factor : float diff --git a/brainglobe_utils/image_io/save.py b/brainglobe_utils/image_io/save.py index f99b29c..163c943 100644 --- a/brainglobe_utils/image_io/save.py +++ b/brainglobe_utils/image_io/save.py @@ -20,7 +20,7 @@ def to_nii(img, dest_path, scale=None, affine_transform=None): img : nifty image object or np.ndarray A nifty image object or numpy array representing a brain. - dest_path : str or Path + dest_path : str The file path where to save the brain. scale : tuple of floats, optional @@ -49,7 +49,7 @@ def to_tiff(img_volume, dest_path, photometric="minisblack"): img_volume : np.ndarray The image to be saved. - dest_path : str or Path + dest_path : str The file path where to save the tiff stack. photometric: str @@ -99,7 +99,7 @@ def to_nrrd(img_volume, dest_path): img_volume : np.ndarray The image to be saved. - dest_path : str or Path + dest_path : str The file path where to save the nrrd image. """ dest_path = str(dest_path) diff --git a/tests/tests/test_image_io.py b/tests/tests/test_image_io.py index 7215472..fcf283c 100644 --- a/tests/tests/test_image_io.py +++ b/tests/tests/test_image_io.py @@ -35,6 +35,15 @@ def write_tiff_sequence_with_txt_file(txt_path, image_array, shuffle=False): The tiff sequence will be saved to a sub-folder inside the same folder as the text file. + + Parameters + ---------- + txt_path : pathlib.Path + Filepath of text file to create + image_array : np.ndarray + Image to write as sequence of tiffs + shuffle : bool + Whether to shuffle the order of filepaths in the text file """ directory = txt_path.parent @@ -56,6 +65,13 @@ def save_any(file_path, image_array): """ Save image_array to given file path, using the save function matching its file extension + + Parameters + ---------- + file_path : pathlib.Path + File path of image to save + image_array : np.ndarray + Image to save """ if file_path.is_dir(): save.to_tiffs(image_array, str(file_path / "image_array")) @@ -64,13 +80,13 @@ def save_any(file_path, image_array): write_tiff_sequence_with_txt_file(file_path, image_array) elif file_path.suffix == ".tif" or file_path.suffix == ".tiff": - save.to_tiff(image_array, file_path) + save.to_tiff(image_array, str(file_path)) elif file_path.suffix == ".nrrd": - save.to_nrrd(image_array, file_path) + save.to_nrrd(image_array, str(file_path)) elif file_path.suffix == ".nii": - save.to_nii(image_array, file_path) + save.to_nii(image_array, str(file_path), scale=(1, 1, 1)) def test_tiff_io(tmp_path, image_array): @@ -78,9 +94,9 @@ def test_tiff_io(tmp_path, image_array): Test that a 2D/3D tiff can be written and read correctly """ dest_path = tmp_path / "image_array.tiff" - save.to_tiff(image_array, dest_path) + save_any(dest_path, image_array) - reloaded = load.load_img_stack(dest_path, 1, 1, 1) + reloaded = load.load_img_stack(str(dest_path), 1, 1, 1) assert (reloaded == image_array).all() @@ -95,10 +111,10 @@ def test_3d_tiff_scaling( Test that a 3D tiff is scaled correctly on loading """ dest_path = tmp_path / "image_array.tiff" - save.to_tiff(array_3d, dest_path) + save_any(dest_path, array_3d) reloaded = load.load_img_stack( - dest_path, x_scaling_factor, y_scaling_factor, z_scaling_factor + str(dest_path), x_scaling_factor, y_scaling_factor, z_scaling_factor ) assert reloaded.shape[0] == array_3d.shape[0] * z_scaling_factor assert reloaded.shape[1] == array_3d.shape[1] * y_scaling_factor @@ -117,7 +133,7 @@ def test_tiff_sequence_io(tmp_path, array_3d, load_parallel): Test that a 3D image can be written and read correctly as a sequence of 2D tiffs (with or without parallel loading) """ - save.to_tiffs(array_3d, str(tmp_path / "image_array")) + save_any(tmp_path, array_3d) reloaded_array = load.load_from_folder( str(tmp_path), 1, 1, 1, load_parallel=load_parallel ) @@ -134,7 +150,7 @@ def test_tiff_sequence_scaling( """ Test that a tiff sequence is scaled correctly on loading """ - save.to_tiffs(array_3d, str(tmp_path / "image_array")) + save_any(tmp_path, array_3d) reloaded_array = load.load_from_folder( str(tmp_path), x_scaling_factor, y_scaling_factor, z_scaling_factor ) @@ -150,7 +166,7 @@ def test_load_img_sequence_from_txt(tmp_path, array_3d): ordered list of the tiff file paths (one per line) """ img_sequence_file = tmp_path / "imgs_file.txt" - write_tiff_sequence_with_txt_file(img_sequence_file, array_3d) + save_any(img_sequence_file, array_3d) # Load image from paths in text file reloaded_array = load.load_img_sequence(str(img_sequence_file), 1, 1, 1) @@ -184,19 +200,19 @@ def test_nii_io(tmp_path, array_3d): """ Test that a 3D image can be written and read correctly as nii """ - nii_path = str(tmp_path / "test_array.nii") - save.to_nii(array_3d, nii_path, scale=(1, 1, 1)) - assert (load.load_nii(nii_path).get_fdata() == array_3d).all() + nii_path = tmp_path / "test_array.nii" + save_any(nii_path, array_3d) + assert (load.load_nii(str(nii_path)).get_fdata() == array_3d).all() def test_nii_read_to_numpy(tmp_path, array_3d): """ Test that conversion of loaded nii image to an in-memory numpy array works """ - nii_path = str(tmp_path / "test_array.nii") - save.to_nii(array_3d, nii_path, scale=(1, 1, 1)) + nii_path = tmp_path / "test_array.nii" + save_any(nii_path, array_3d) - reloaded_array = load.load_nii(nii_path, as_array=True, as_numpy=True) + reloaded_array = load.load_nii(str(nii_path), as_array=True, as_numpy=True) assert (reloaded_array == array_3d).all() @@ -204,9 +220,9 @@ def test_nrrd_io(tmp_path, array_3d): """ Test that a 3D image can be written and read correctly as nrrd """ - nrrd_path = str(tmp_path / "test_array.nrrd") - save.to_nrrd(array_3d, nrrd_path) - assert (load.load_nrrd(nrrd_path) == array_3d).all() + nrrd_path = tmp_path / "test_array.nrrd" + save_any(nrrd_path, array_3d) + assert (load.load_nrrd(str(nrrd_path)) == array_3d).all() @pytest.mark.parametrize( @@ -227,7 +243,7 @@ def test_load_any(tmp_path, array_3d, file_name): src_path = tmp_path / file_name save_any(src_path, array_3d) - assert (load.load_any(src_path) == array_3d).all() + assert (load.load_any(str(src_path)) == array_3d).all() def test_load_any_error(tmp_path): @@ -235,7 +251,7 @@ def test_load_any_error(tmp_path): Test that load_any throws an error for an unknown file extension """ with pytest.raises(NotImplementedError): - load.load_any(tmp_path / "test.unknown") + load.load_any(str(tmp_path / "test.unknown")) def test_scale_z(array_3d): @@ -246,20 +262,22 @@ def test_scale_z(array_3d): assert utils.scale_z(array_3d, 2).shape[0] == array_3d.shape[0] * 2 -def test_image_size_dir(tmp_path, array_3d): - save.to_tiffs(array_3d, str(tmp_path / "image_array")) - - image_shape = load.get_size_image_from_file_paths(str(tmp_path)) - assert image_shape["x"] == array_3d.shape[2] - assert image_shape["y"] == array_3d.shape[1] - assert image_shape["z"] == array_3d.shape[0] - - -def test_image_size_txt(tmp_path, array_3d): - txt_filepath = tmp_path / "image.txt" - write_tiff_sequence_with_txt_file(txt_filepath, array_3d) +@pytest.mark.parametrize( + "file_name", + [ + "test_array.txt", + pytest.param("", id="dir of tiffs"), + ], +) +def test_image_size(tmp_path, array_3d, file_name): + """ + Test that image size can be detected from a directory of 2D tiffs, or + a text file containing the paths of a sequence of 2D tiffs + """ + file_path = tmp_path / file_name + save_any(file_path, array_3d) - image_shape = load.get_size_image_from_file_paths(str(txt_filepath)) + image_shape = load.get_size_image_from_file_paths(str(file_path)) assert image_shape["x"] == array_3d.shape[2] assert image_shape["y"] == array_3d.shape[1] assert image_shape["z"] == array_3d.shape[0] From b3bd48578d848b9215b747ea1c7105006364c28e Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <k.meechan@ucl.ac.uk> Date: Thu, 14 Mar 2024 11:06:31 +0000 Subject: [PATCH 9/9] split shuffle into a fixture --- tests/tests/test_image_io.py | 39 +++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/tests/tests/test_image_io.py b/tests/tests/test_image_io.py index fcf283c..1ff37a5 100644 --- a/tests/tests/test_image_io.py +++ b/tests/tests/test_image_io.py @@ -28,7 +28,27 @@ def image_array(request, array_2d, array_3d): return array_3d -def write_tiff_sequence_with_txt_file(txt_path, image_array, shuffle=False): +@pytest.fixture() +def shuffled_txt_path(tmp_path, array_3d): + """ + Return the path to a text file containing the paths of a series of 2D tiffs + in a random order + """ + txt_path = tmp_path / "imgs_file.txt" + write_tiff_sequence_with_txt_file(txt_path, array_3d) + + # Shuffle paths in the text file into a random order + with open(txt_path, "r+") as f: + tiff_paths = f.read().splitlines() + random.Random(4).shuffle(tiff_paths) + f.seek(0) + f.writelines(line + "\n" for line in tiff_paths) + f.truncate() + + return txt_path + + +def write_tiff_sequence_with_txt_file(txt_path, image_array): """ Write an image array to a series of tiffs, and write a text file containing all the tiff file paths in order (one per line). @@ -42,8 +62,6 @@ def write_tiff_sequence_with_txt_file(txt_path, image_array, shuffle=False): Filepath of text file to create image_array : np.ndarray Image to write as sequence of tiffs - shuffle : bool - Whether to shuffle the order of filepaths in the text file """ directory = txt_path.parent @@ -54,8 +72,6 @@ def write_tiff_sequence_with_txt_file(txt_path, image_array, shuffle=False): # Write txt file containing all tiff file paths (one per line) tiff_paths = sorted(sub_dir.iterdir()) - if shuffle: - random.Random(4).shuffle(tiff_paths) txt_path.write_text( "\n".join([str(sub_dir / fname) for fname in tiff_paths]) ) @@ -177,18 +193,13 @@ def test_load_img_sequence_from_txt(tmp_path, array_3d): "sort", [True, False], ) -def test_sort_img_sequence_from_txt(tmp_path, array_3d, sort): +def test_sort_img_sequence_from_txt(shuffled_txt_path, array_3d, sort): """ - Test that filepaths read from a txt file can be sorted correctly + Test that shuffled filepaths read from a txt file can be sorted correctly """ - img_sequence_file = tmp_path / "imgs_file.txt" - write_tiff_sequence_with_txt_file( - img_sequence_file, array_3d, shuffle=True - ) - - # Load image from paths in text file + # Load image from shuffled paths in text file reloaded_array = load.load_img_sequence( - str(img_sequence_file), 1, 1, 1, sort=sort + str(shuffled_txt_path), 1, 1, 1, sort=sort ) if sort: assert (reloaded_array == array_3d).all()