Skip to content

Commit

Permalink
Merge pull request #675 from AFM-SPM/SylviaWhittle/manual-asd
Browse files Browse the repository at this point in the history
As last person to push to this branch my approval isn't formally accepted. The two actions I've just taken were to...

+ Resolve merge conflicts
+ Remove a `print()` statement

...and so I'm happy that these are not substantial and @SylviaWhittle changes are ok to merge.
  • Loading branch information
ns-rse committed Dec 19, 2023
2 parents 3df029a + d107dd8 commit 3f94eb5
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 27 deletions.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ dependencies = [
"snoop",
"tifffile",
"tqdm",
"pyfiglet",
"h5py",
"topofileformats"
]

[project.optional-dependencies]
Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,12 @@ def load_scan_topostats() -> LoadScans:
return LoadScans([RESOURCES / "file.topostats"], channel="dummy_channel")


@pytest.fixture()
def load_scan_asd() -> LoadScans:
"""Instantiate a LoadScans object from a .asd file."""
return LoadScans([RESOURCES / "file.asd"], channel="TP")


# Minicircle fixtures
@pytest.fixture()
def minicircle(load_scan: LoadScans, filter_config: dict) -> Filters:
Expand Down
Binary file added tests/resources/file.asd
Binary file not shown.
67 changes: 59 additions & 8 deletions tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ def test_write_yaml(tmp_path: Path) -> None:

def test_path_to_str(tmp_path: Path) -> None:
"""Test that Path objects are converted to strings."""
CONFIG_PATH = {"this": "is", "a": "test", "with": tmp_path, "and": {"nested": tmp_path / "nested"}}
CONFIG_PATH = {
"this": "is",
"a": "test",
"with": tmp_path,
"and": {"nested": tmp_path / "nested"},
}
CONFIG_STR = path_to_str(CONFIG_PATH)

assert isinstance(CONFIG_STR, dict)
Expand Down Expand Up @@ -183,7 +188,10 @@ def test_read_gwy_component_dtype() -> None:
@pytest.mark.parametrize(
("input_paths", "expected_paths"),
[
([Path("a/b/c/d"), Path("a/b/e/f"), Path("a/b/g"), Path("a/b/h")], ["c/d", "e/f", "g", "h"]),
(
[Path("a/b/c/d"), Path("a/b/e/f"), Path("a/b/g"), Path("a/b/h")],
["c/d", "e/f", "g", "h"],
),
(["a/b/c/d", "a/b/e/f", "a/b/g", "a/b/h"], ["c/d", "e/f", "g", "h"]),
(["g", "a/b/e/f", "a/b/g", "a/b/h"], ["g", "a/b/e/f", "a/b/g", "a/b/h"]),
(["a/b/c/d"], ["a/b/c/d"]),
Expand Down Expand Up @@ -263,7 +271,12 @@ def test_convert_basename_to_relative_paths():
Path("output/here/images/today/test"),
),
# Relative path, nested under base_dir, no file suffix
(Path("/some/random/path"), Path("images/"), Path("output/here"), Path("output/here/images/")),
(
Path("/some/random/path"),
Path("images/"),
Path("output/here"),
Path("output/here/images/"),
),
# Absolute path, nested under base_dir, output not nested under base_dir, with file_suffix
(
Path("/some/random/path"),
Expand Down Expand Up @@ -297,7 +310,11 @@ def test_get_out_path(image_path: Path, base_dir: Path, output_dir: Path, expect
def test_get_out_path_attributeerror() -> None:
"""Test get_out_path() raises AttribteError when passed a string instead of a Path() for image_path."""
with pytest.raises(AttributeError):
get_out_path(image_path="images/test.spm", base_dir=Path("/some/random/path"), output_dir=Path("output/here"))
get_out_path(
image_path="images/test.spm",
base_dir=Path("/some/random/path"),
output_dir=Path("output/here"),
)


def test_save_folder_grainstats(tmp_path: Path) -> None:
Expand Down Expand Up @@ -359,6 +376,27 @@ def test_load_scan_gwy(load_scan_gwy: LoadScans) -> None:
assert px_to_nm_scaling == 0.8468632812499975


def test_load_scan_asd_file_not_found() -> None:
"""Test file not found exception is raised when loading non existent .ASD file."""
load_scan_asd = LoadScans([Path("file_does_not_exist.asd")], channel="TP")
load_scan_asd.img_path = load_scan_asd.img_paths[0]
load_scan_asd.filename = load_scan_asd.img_paths[0].stem
with pytest.raises(FileNotFoundError):
load_scan_asd.load_asd()


def test_load_scan_asd(load_scan_asd: LoadScans) -> None:
"""Test loading of a .asd file."""
load_scan_asd.img_path = load_scan_asd.img_paths[0]
load_scan_asd.filename = load_scan_asd.img_paths[0].stem
frames, px_to_nm_scaling = load_scan_asd.load_asd()
assert isinstance(frames, np.ndarray)
assert frames.shape == (197, 200, 200)
assert frames.sum() == -71724923530211.84
assert isinstance(px_to_nm_scaling, float)
assert px_to_nm_scaling == 2.0


def test_load_scan_topostats(load_scan_topostats: LoadScans) -> None:
"""Test loading of a .topostats file."""
load_scan_topostats.img_path = load_scan_topostats.img_paths[0]
Expand Down Expand Up @@ -426,6 +464,7 @@ def test_gwy_read_component(load_scan_dummy: LoadScans) -> None:
("load_scan_jpk", 1, (256, 256), 286598232.9308627, "file", 1.2770176335964876),
("load_scan_gwy", 1, (512, 512), 33836850.232917726, "file", 0.8468632812499975),
("load_scan_topostats", 1, (1024, 1024), 182067.12616107278, "file", 0.4940029296875),
("load_scan_asd", 197, (200, 200), -673381139990.2344, "file_122", 2.0),
],
)
def test_load_scan_get_data(
Expand Down Expand Up @@ -465,7 +504,7 @@ def test_load_scan_get_data_check_image_size_and_add_to_dict(
load_scan_spm.filename = "minicircle"
load_scan_spm.img_path = tmp_path
load_scan_spm.image = np.ndarray((x, y))
load_scan_spm._check_image_size_and_add_to_dict()
load_scan_spm._check_image_size_and_add_to_dict(image=load_scan_spm.image, filename=load_scan_spm.filename)
assert log_msg in caplog.text


Expand All @@ -492,7 +531,12 @@ def test_load_pkl() -> None:
None,
np.array([[0, 0, 0], [0, 1, 1], [0, 1, 0]]),
),
(np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), 3.14159265, np.array([[0, 0, 0], [0, 1, 1], [0, 1, 0]]), None),
(
np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]),
3.14159265,
np.array([[0, 0, 0], [0, 1, 1], [0, 1, 0]]),
None,
),
(
np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]),
3.14159265,
Expand All @@ -516,7 +560,9 @@ def test_save_topostats_file(
}

save_topostats_file(
output_dir=tmp_path, filename="topostats_file_test.topostats", topostats_object=topostats_object
output_dir=tmp_path,
filename="topostats_file_test.topostats",
topostats_object=topostats_object,
)

with h5py.File(f"{tmp_path}/topostats_file_test.topostats", "r") as f:
Expand All @@ -529,7 +575,12 @@ def test_save_topostats_file(
if grain_mask_below is not None:
grain_mask_below_read = f["grain_masks/below"][:]

assert hdf5_file_keys == ["grain_masks", "image", "pixel_to_nm_scaling", "topostats_file_version"]
assert hdf5_file_keys == [
"grain_masks",
"image",
"pixel_to_nm_scaling",
"topostats_file_version",
]
assert 0.1 == topostats_file_version_read
np.testing.assert_array_equal(image, image_read)
assert pixel_to_nm_scaling == pixel_to_nm_scaling_read
Expand Down
74 changes: 55 additions & 19 deletions topostats/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import tifffile
from igor2 import binarywave
from ruamel.yaml import YAML, YAMLError
from topofileformats import asd

from topostats.logs.logs import LOGGER_NAME

Expand Down Expand Up @@ -69,7 +70,10 @@ def get_date_time() -> str:


def write_yaml(
config: dict, output_dir: str | Path, config_file: str = "config.yaml", header_message: str = None
config: dict,
output_dir: str | Path,
config_file: str = "config.yaml",
header_message: str = None,
) -> None:
"""Write a configuration (stored as a dictionary) to a YAML file.
Expand Down Expand Up @@ -566,6 +570,26 @@ def load_topostats(self) -> tuple:

return (image, pixel_to_nm_scaling)

def load_asd(self) -> tuple:
"""Extract image and pixel to nm scaling from .asd files.
Returns
-------
tuple: (np.ndarray, float)
A tuple containing the image and its pixel to nanometre scaling value.
"""
try:
frames: np.ndarray
pixel_to_nm_scaling: float
_: dict
frames, pixel_to_nm_scaling, _ = asd.load_asd(file_path=self.img_path, channel=self.channel)
LOGGER.info(f"[{self.filename}] : Loaded image from : {self.img_path}")
except FileNotFoundError:
LOGGER.info(f"[{self.filename}] : File not found. Path: {self.img_path}")
raise

return (frames, pixel_to_nm_scaling)

def load_ibw(self) -> tuple:
"""Load image from Asylum Research (Igor) .ibw files.
Expand Down Expand Up @@ -870,6 +894,7 @@ def get_data(self) -> None:
".ibw": self.load_ibw,
".gwy": self.load_gwy,
".topostats": self.load_topostats,
".asd": self.load_asd,
}

for img_path in self.img_paths:
Expand All @@ -889,44 +914,55 @@ def get_data(self) -> None:
else:
raise
else:
self._check_image_size_and_add_to_dict()
if suffix == ".asd":
for index, frame in enumerate(self.image):
self._check_image_size_and_add_to_dict(image=frame, filename=f"{self.filename}_{index}")
else:
self._check_image_size_and_add_to_dict(image=self.image, filename=self.filename)
else:
raise ValueError(
f"File type {suffix} not yet supported. Please make an issue at \
https://github.com/AFM-SPM/TopoStats/issues, or email topostats@sheffield.ac.uk to request support for \
this file type."
)

def _check_image_size_and_add_to_dict(self) -> None:
def _check_image_size_and_add_to_dict(self, image: np.ndarray, filename: str) -> None:
"""Check the image is above a minimum size in both dimensions.
Images that do not meet the minimum size are not included for processing.
Parameters
----------
image: np.ndarray
An array of the extracted AFM image.
filename: str
The name of the file
"""
if self.image.shape[0] < self.MINIMUM_IMAGE_SIZE or self.image.shape[1] < self.MINIMUM_IMAGE_SIZE:
LOGGER.warning(f"[{self.filename}] Skipping, image too small: {self.image.shape}")
if image.shape[0] < self.MINIMUM_IMAGE_SIZE or image.shape[1] < self.MINIMUM_IMAGE_SIZE:
LOGGER.warning(f"[{filename}] Skipping, image too small: {image.shape}")
else:
self.add_to_dict()
LOGGER.info(f"[{self.filename}] Image added to processing.")
self.add_to_dict(image=image, filename=filename)
LOGGER.info(f"[{filename}] Image added to processing.")

def add_to_dict(self, image: np.ndarray, filename: str) -> None:
"""Add an image and metadata to the img_dict dictionary under the key filename.
def add_to_dict(self) -> None:
"""Add image, image path and pixel to nanometre scaling to the img_dic dictionary under key filename.
Adds the image and associated metadata such as any grain masks, and pixel to nanometere
scaling factor to the img_dict dictionary which is used as a place to store the image
information for processing.
Parameters
----------
filename: str
The filename, idealy without an extension.
image: np.ndarray
An array of the extracted AFM image.
img_path: str
The path to the AFM file (with a frame number if applicable)
px_2_nm: float
The length of a pixel in nm.
filename: str
The name of the file
"""
self.img_dict[self.filename] = {
"filename": self.filename,
"img_path": self.img_path.with_name(self.filename),
self.img_dict[filename] = {
"filename": filename,
"img_path": self.img_path.with_name(filename),
"pixel_to_nm_scaling": self.pixel_to_nm_scaling,
"image_original": self.image,
"image_original": image,
"image_flattened": None,
"grain_masks": self.grain_masks,
}
Expand Down

0 comments on commit 3f94eb5

Please sign in to comment.