Source code for camacq.image

"""Handle images."""

from __future__ import annotations

from collections import defaultdict
import logging
from typing import Any

import numpy as np
from numpy import typing as npt
import tifffile
import xmltodict

_LOGGER = logging.getLogger(__name__)


[docs] def read_image(path: str) -> npt.NDArray[Any] | None: """Read a tif image and return the data. Parameters ---------- path : str The path to the image. Returns ------- numpy array Return a numpy array with image data. """ try: return tifffile.imread(path, key=0) except OSError as exception: _LOGGER.error("Bad path to image: %s", exception) return None
[docs] def save_image( path: str, data: npt.NDArray[Any], description: str | None = None ) -> None: """Save a tif image with image data and meta data. Parameters ---------- path : str The path to the image. data : numpy array A numpy array with the image data. description : str The description string of the image. """ tifffile.imwrite(path, data, description=description)
[docs] def make_proj(images: dict[str, int]) -> dict[int, ImageData]: """Make a dict of max projections from a dict of channels and paths. Each channel will make one max projection. Parameters ---------- images : dict Dict of paths and channel ids. Returns ------- dict Return a dict of channels that map ImageData objects. Each image object have a max projection as data. """ _LOGGER.info("Making max projections...") sorted_images: dict[int, list[ImageData]] = defaultdict(list) max_imgs: dict[int, ImageData] = {} for path, channel in images.items(): image = ImageData(path=path) # Exclude images with 0, 16 or 256 pixel side. if len(image.data) == 0 or len(image.data) == 16 or len(image.data) == 256: continue sorted_images[channel].append(image) proj = np.max([img.data for img in sorted_images[channel]], axis=0) max_imgs[channel] = ImageData(path=path, data=proj, metadata=image.metadata) return max_imgs
[docs] class ImageData: """Represent the data of an image with path, data, metadata and histogram. Parameters ---------- path : str Path to the image. data : numpy array A numpy array with the image data. metadata : dict The meta data of the image as a JSON dict. Attributes ---------- path : str The path to the image. """ def __init__( self, path: str | None = None, data: npt.NDArray[Any] | None = None, metadata: dict[str, Any] | None = None, ) -> None: """Set up instance.""" self.path = path self._data = data self.description: str | None = None if metadata is not None: self.metadata = metadata @property def data(self) -> npt.NDArray[Any]: """:numpy array: Return the data of the image. :setter: Set the data of the image. """ if self._data is None: self._load_image_data() assert self._data is not None # noqa: S101 return self._data @data.setter def data(self, value: npt.NDArray[Any]) -> None: """Set the data of the image.""" self._data = value @property def metadata(self) -> dict[str, Any]: """:str: Return metadata of image. :setter: Set the meta data of the image. """ if self.description is None: self._load_image_data() description = self.description if description is None: return {} return xmltodict.parse(description) @metadata.setter def metadata(self, value: dict[str, Any]) -> None: """Set the metadata of the image.""" self.description = xmltodict.unparse(value) @property def histogram(self) -> tuple[npt.NDArray[Any], npt.NDArray[Any]]: """:numpy array: Calculate and return image histogram.""" if self._data is None: self._load_image_data() data = self._data assert data is not None # noqa: S101 if data.dtype.name == "uint16": max_int = 65535 else: max_int = 255 return np.histogram(data, bins=256, range=(0, max_int)) def _load_image_data(self) -> None: """Load image data from path.""" if self.path is None: _LOGGER.error("Cannot load image data: path is None") return try: with tifffile.TiffFile(self.path) as tif: self._data = tif.asarray(key=0) page = tif.pages[0] self.description = getattr(page, "description", "") except (OSError, ValueError) as exception: _LOGGER.error("Bad path %s to image: %s", self.path, exception)
[docs] def save( self, path: str | None = None, data: npt.NDArray[Any] | None = None, metadata: dict[str, Any] | None = None, ) -> None: """Save image with image data and optional meta data. Parameters ---------- path : str The path to the image. data : numpy array A numpy array with the image data. metadata : dict The meta data of the image as a JSON dict. """ if path is None: path = self.path if data is None: data = self.data if metadata is None: metadata = self.metadata description = xmltodict.unparse(metadata) save_image(path, data, description) # type: ignore[arg-type]
def __repr__(self) -> str: """Return the representation.""" return f"ImageData(path={self.path})"