Skip to content

Commit

Permalink
Merge pull request #34 from AFM-SPM/SylviaWhittle/file-type-ibw
Browse files Browse the repository at this point in the history
Add file type: IBW
  • Loading branch information
SylviaWhittle authored May 1, 2024
2 parents 6502c56 + 85ffb11 commit 4eafdff
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 6 deletions.
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ for use with [TopoStats](https://github.com/AFM-SPM/TopoStats).

Supported file formats

| File format | Description |
|-------------|------------------|
| `.asd` | High-speed AFM |
| File format | Description |
|-------------|----------------|
| `.asd` | High-speed AFM |
| `.ibw` | [WaveMetrics](https://www.wavemetrics.com/) |
| `.spm` | [Bruker's Format](https://www.bruker.com/) |
| `.jpk` | [Bruker](https://www.bruker.com/) |

Expand All @@ -30,7 +31,6 @@ awaiting refactoring to move their functionality into topofileformats these are

| File format | Description | Status |
|-------------|---------------------------------------------------------|--------------------------------------------|
| `.ibw` | [WaveMetrics](https://www.wavemetrics.com/) | TopoStats supported, to be migrated (#17). |
| `.gwy` | [Gwyddion](http://gwyddion.net/) | TopoStats supported, to be migrated (#1). |
| `.nhf` | [Nanosurf](https://www.nanosurf.com/en/) | To Be Implemented. |
| `.aris` | [Imaris Oxford Instruments](https://imaris.oxinst.com/) | To Be Implemented. |
Expand Down Expand Up @@ -69,6 +69,18 @@ from topofileformats.asd import load_asd
frames, pixel_to_nanometre_scaling_factor, metadata = load_asd(file_path="./my_asd_file.asd", channel="TP")
```

### .ibw

You can open `.ibw` files using the `load_ibw` function. Just pass in the path to the file
and the channel name that you want to use. (If in doubt, use `HeightTracee` (yes, with the
extra 'e'), `ZSensorTrace`, or `ZSensor`).

```python
from topofileformats.ibw import load_ibw

image, pixel_to_nanometre_scaling_factor = load_ibw(file_path="./my_ibw_file.ibw", channel="HeightTracee")
```

### .jpk

You can open `.jpk` files using the `load_jpk` function. Just pass in the path
Expand Down
82 changes: 82 additions & 0 deletions examples/example_01.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,88 @@
"image, pixel_to_nm_scaling = load_jpk(file_path=FILE, channel=\"height_trac\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Plot the image\n",
"import matplotlib.pyplot as plt\n",
"\n",
"plt.imshow(image, cmap=\"afmhot\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# IBW Files"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Import the load_ibw function from topofileformats\n",
"from topofileformats.ibw import load_ibw"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Load the IBW file as an image and pixel to nm scaling factor\n",
"FILE = \"../tests/resources/sample_0.ibw\"\n",
"image, pixel_to_nm_scaling = load_ibw(file_path=FILE, channel=\"HeightTracee\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Plot the image\n",
"import matplotlib.pyplot as plt\n",
"\n",
"plt.imshow(image, cmap=\"afmhot\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# IBW Files"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Import the load_ibw function from topofileformats\n",
"from topofileformats.ibw import load_ibw"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Load the IBW file as an image and pixel to nm scaling factor\n",
"FILE = \"../tests/resources/sample_0.ibw\"\n",
"image, pixel_to_nm_scaling = load_ibw(file_path=FILE, channel=\"HeightTracee\")"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ keywords = [

dependencies = [
"matplotlib",
"igor2",
"tifffile",
"pySPM",
"pySPM",
"loguru",
]

Expand Down
Binary file added tests/resources/sample_0.ibw
Binary file not shown.
37 changes: 37 additions & 0 deletions tests/test_ibw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Test the loading of ibw files."""

from pathlib import Path
import pytest

import numpy as np

from topofileformats.ibw import load_ibw

BASE_DIR = Path.cwd()
RESOURCES = BASE_DIR / "tests" / "resources"


@pytest.mark.parametrize(
("file_name", "channel", "pixel_to_nm_scaling", "image_shape", "image_dtype", "image_sum"),
[pytest.param("sample_0.ibw", "HeightTracee", 1.5625, (512, 512), "f4", -218091520.0, id="test image 0")],
)
def test_load_ibw(
file_name: str,
channel: str,
pixel_to_nm_scaling: float,
image_shape: tuple[int, int],
image_dtype: type,
image_sum: float,
) -> None:
"""Test the normal operation of loading an .ibw file."""
result_image = np.ndarray
result_pixel_to_nm_scaling = float

file_path = RESOURCES / file_name
result_image, result_pixel_to_nm_scaling = load_ibw(file_path, channel)

assert result_pixel_to_nm_scaling == pixel_to_nm_scaling
assert isinstance(result_image, np.ndarray)
assert result_image.shape == image_shape
assert result_image.dtype == image_dtype
assert result_image.sum() == image_sum
2 changes: 1 addition & 1 deletion topofileformats/asd.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def load_asd(file_path: Path, channel: str):
# Ensure the file path is a Path object
file_path = Path(file_path)
# Open the file in binary mode
with Path.open(file_path, "rb", encoding=None) as open_file: # pylint: disable=W1514
with Path.open(file_path, "rb", encoding=None) as open_file: # pylint: disable=unspecified-encoding
file_version = read_file_version(open_file)

if file_version == 0:
Expand Down
96 changes: 96 additions & 0 deletions topofileformats/ibw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""For decoding and loading .ibw AFM file format into Python Numpy arrays."""

from __future__ import annotations
from pathlib import Path

import numpy as np
from igor2 import binarywave

from topofileformats.logging import logger

logger.enable(__package__)


def _ibw_pixel_to_nm_scaling(scan: dict) -> float:
"""
Extract pixel to nm scaling from the IBW image metadata.
Parameters
----------
scan : dict
The loaded binary wave object.
Returns
-------
float
A value corresponding to the real length of a single pixel.
"""
# Get metadata
notes = {}
for line in str(scan["wave"]["note"]).split("\\r"):
if line.count(":"):
key, val = line.split(":", 1)
notes[key] = val.strip()
# Has potential for non-square pixels but not yet implemented
return (
float(notes["SlowScanSize"]) / scan["wave"]["wData"].shape[0] * 1e9, # as in m
float(notes["FastScanSize"]) / scan["wave"]["wData"].shape[1] * 1e9, # as in m
)[0]


def load_ibw(file_path: Path | str, channel: str) -> tuple[np.ndarray, float]:
"""
Load image from Asylum Research (Igor) .ibw files.
Parameters
----------
file_path: Path | str
Path to the .ibw file.
channel: str
The channel to extract from the .ibw file.
Returns
-------
tuple[np.ndarray, float]
A tuple containing the image and its pixel to nanometre scaling value.
Raises
------
FileNotFoundError
If the file is not found.
ValueError
If the channel is not found in the .ibw file.
Examples
--------
Load the image and pixel to nanometre scaling factor - 'HeightTracee' is the default channel name (the extra 'e' is
not a typo!).
>>> from topofileformats.ibw import load_ibw
>>> image, pixel_to_nanometre_scaling_factor = load_ibw(file_path="./my_ibw_file.ibw", channel="HeightTracee")
"""
logger.info(f"Loading image from : {file_path}")
file_path = Path(file_path)
filename = file_path.stem
try:
scan = binarywave.load(file_path)
logger.info(f"[{filename}] : Loaded image from : {file_path}")
labels = []
for label_list in scan["wave"]["labels"]:
for label in label_list:
if label:
labels.append(label.decode())
channel_idx = labels.index(channel)
image = scan["wave"]["wData"][:, :, channel_idx].T * 1e9 # Looks to be in m
image = np.flipud(image)
logger.info(f"[{filename}] : Extracted channel {channel}")
except FileNotFoundError:
logger.error(f"[{filename}] File not found : {file_path}")
except ValueError:
logger.error(f"[{filename}] : {channel} not in {file_path.suffix} channel list: {labels}")
raise
except Exception as e:
logger.error(f"[{filename}] : {e}")
raise e

return (image, _ibw_pixel_to_nm_scaling(scan))

0 comments on commit 4eafdff

Please sign in to comment.