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()