Source code for neuralib.imglib.io

from pathlib import Path

import cv2
import numpy as np
import numpy.typing as npt
from neuralib.imglib.norm import normalize_sequences
from neuralib.typing import PathLike
from PIL import Image
from tifffile import tifffile
from tqdm import tqdm

__all__ = [
    'load_sequence',
    'read_avi',
    'read_pdf',
    'write_avi',
    'tif_to_gif',
    'gif_show'
]


[docs] def load_sequence(path: PathLike, suffix: str = '*.tif') -> np.ndarray: """ Recursively load a TIFF sequence from a file or directory using memory mapping. - If `path` is a TIFF file, returns a memmapped NumPy array. - If `path` is a directory, loads and stacks all valid TIFF files matching `suffix`. - Ignores hidden files (starting with '.') or AppleDouble files (._). :param path: Path to a .tif file or a directory of .tif files. :param suffix: Glob pattern for selecting TIFF files (default: ``*.tif``). :return: Stacked array of TIFF frames. ``Array[Any, [H, W]]`` :raise RuntimeError: If the path is not found or no valid TIFFs were found. """ path = Path(path) if path.is_file(): return tifffile.memmap(path) elif path.is_dir(): files = sorted([ f for f in path.glob(suffix) if f.is_file() and not f.name.startswith('._') and not f.name.startswith('.') ]) if not files: raise FileNotFoundError(f'No valid TIFF files found in directory: {path}') arrays = [] for f in tqdm(files, desc='Loading TIFF sequence', unit='file'): try: arrays.append(tifffile.memmap(f)) except Exception as e: print(f'Skipped invalid TIFF file: {f.name} ({e})') if not arrays: raise ValueError(f'All files in {path} failed to load as TIFFs.') return np.vstack(arrays) else: raise FileNotFoundError(f'Path not found: {path}')
[docs] def read_avi(avi_file: PathLike, grey_scale: bool = True) -> np.ndarray: """Read a sequences file (i.e., AVI/MP4) into an array, converting to grayscale if specified. :param avi_file: Path to the AVI file. :param grey_scale: Whether to convert frames to grayscale. :return: Sequences array. ``Array[uint8, [F, W, H] | [F, W, H, 3]]`` """ cap = cv2.VideoCapture(str(avi_file)) if not cap.isOpened(): raise RuntimeError(f'Error opening AVI file: {avi_file}') frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) if grey_scale: shape = (frame_count, height, width) else: shape = (frame_count, height, width, 3) frames = np.empty(shape, dtype=np.uint8) i = 0 try: while True: ret, frame = cap.read() if not ret: break if grey_scale: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) frames[i] = frame i += 1 finally: cap.release() return frames
[docs] def read_pdf(file: PathLike, single_image: bool = True, poppler_path: str | None = None, **kwargs) -> np.ndarray: r""" Read pdf as an image array :param file: :param single_image: :param poppler_path: if poppler not installed in a system level. Specify the path. for example: ``Release-23.08.0-0\poppler-23.08.0\Library\bin`` .. seealso:: `<https://stackoverflow.com/questions/53481088/poppler-in-path-for-pdf2image>`_ :param kwargs: pass through `convert_from_path` :return: Image array """ from pdf2image import convert_from_path from pdf2image.exceptions import PDFInfoNotInstalledError file = str(file) if single_image: try: conv = convert_from_path(file, **kwargs)[0] except PDFInfoNotInstalledError: if poppler_path is None: raise RuntimeError('download poppler first. or using apt-get / brew depending on OS') conv = convert_from_path(file, poppler_path=poppler_path, **kwargs)[0] else: raise NotImplementedError('multi-pages pdf not currently support') return cv2.cvtColor(np.array(conv), cv2.COLOR_BGR2RGB)
[docs] def tif_to_gif(image_file: PathLike, output_path: PathLike, fps: int = 30, **kwargs) -> None: """Convert tif sequences to GIF""" import imageio frames = imageio.mimread(str(image_file)) frames = [np.asarray(frame) for frame in frames] frames = normalize_sequences(frames, **kwargs) gif_frames: list[npt.ArrayLike] = [frame for frame in frames] imageio.mimsave(str(output_path), gif_frames, duration=1 / fps)
[docs] def gif_show(file: PathLike) -> None: """ Display GIF file in a cv2 window :param file: Path to the GIF file to be displayed. :raises ValueError: If the provided file is not a GIF. """ file = Path(file) if not file.suffix == '.gif': raise ValueError('must be gif file!') gif = Image.open(file) while True: try: frame = np.array(gif.convert('RGB')) frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) cv2.imshow('GIF', frame) if cv2.waitKey(100) & 0xFF == ord('q'): break gif.seek(gif.tell() + 1) except EOFError: gif.seek(0) gif.close() cv2.destroyAllWindows()
[docs] def write_avi(file_path: str, frames: np.ndarray, fps: float = 30.0) -> None: """ Write a sequence of frames to an AVI file. :param file_path: The path where the AVI file will be saved. :param frames: `Array[uint, [F, W, H]|[F, W, H, 3]]` containing the frames. :param fps: The frames per second (frame rate) of the output video. """ if not file_path.endswith('avi'): raise ValueError('file path need to write to *.avi') if not len(frames): raise ValueError("Frames array is empty.") height, width = frames[0].shape[:2] if frames[0].ndim == 3: is_color = True else: is_color = False fourcc = cv2.VideoWriter.fourcc(*'MJPG') out = cv2.VideoWriter(file_path, fourcc, fps, (width, height), isColor=is_color) for frame in tqdm(frames, desc='write frames to avi', unit='frames'): # frame = frame[:, :, [2, 1, 0]] # Convert RGB to BGR out.write(frame) out.release()