Source code for playnano.io.formats.read_jpk_folder

"""
Module to load .jpk AFM data files from a folder into Python NumPy arrays.

Files contained within the same folder are read together.
Files read with the height data in nm.
"""

import logging
from pathlib import Path

import numpy as np
import tifffile
from AFMReader.jpk import load_jpk

from playnano.afm_stack import AFMImageStack

logger = logging.getLogger(__name__)


def _extract_scan_rate(jpk_file: Path) -> float:
    """
    Extract the scan rate in lines per second from a .jpk image file.

    Parameters
    ----------
    jpk_file : Path
        Path to a .jpk file.

    Returns
    -------
    float
        The scan rate of the image in fast scan lines per second.
    """
    with tifffile.TiffFile(jpk_file) as tif:
        return (
            tif.pages[0].tags["32841"].value
        )  # Return the Scan Rate attribute from the tiff tag value.


[docs] def load_jpk_folder( folder_path: Path | str, channel: str, flip_image: bool = True ) -> AFMImageStack: """ Load an AFM video from a folder of individual .jpk image files. AFMReader converts "height", "measuredHeight" and "amplitude" channels to nm. Parameters ---------- folder_path : Path | str Path to folder containing .jpk files. channel : str Channel to extract. flip_image : bool, optional Flip each image vertically if True. Returns ------- AFMImageStack Loaded AFM image stack with metadata and per-frame info. """ folder = Path(folder_path) if not folder.is_dir(): raise ValueError(f"{folder} is not a directory.") jpk_files = sorted(folder.glob("*.jpk")) if not jpk_files: raise FileNotFoundError(f"No .jpk files found in {folder}.") logger.info(f"Found {len(jpk_files)} .jpk files.") # Load first image to get shape and pixel size first_img, first_pixel_size_nm = load_jpk(jpk_files[0], channel) if flip_image: first_img = np.flipud(first_img) height_px, width_px = first_img.shape dtype = first_img.dtype # Preallocate image stack num_frames = len(jpk_files) image_stack = np.empty((num_frames, height_px, width_px), dtype=dtype) # Extract metadata from first image # Line rate and timestamps line_rate = _extract_scan_rate(jpk_files[0]) # lines per second lines_per_frame = height_px # number of fast scan lines in an image frame_rate = line_rate / lines_per_frame # frames per second frame_interval = 1.0 / frame_rate # time taken per frame timestamps = np.arange(num_frames) * frame_interval # Load all images for i, fpath in enumerate(jpk_files): logger.debug(f"Loading {fpath.name}") img, px_size_nm = load_jpk(fpath, channel) if img.shape != (height_px, width_px): raise ValueError(f"Inconsistent image shape in {fpath}") if not np.isclose(px_size_nm, first_pixel_size_nm): raise ValueError(f"Inconsistent pixel size in {fpath}") if flip_image: img = np.flipud(img) image_stack[i] = img # Compose per-frame metadata list frame_metadata = [] for ts in timestamps: frame_metadata.append({"timestamp": ts, "line_rate": line_rate}) return AFMImageStack( data=image_stack, pixel_size_nm=first_pixel_size_nm, channel=channel, file_path=str(folder), frame_metadata=frame_metadata, )