Source code for neuroconv.datainterfaces.image.baseimageinterface

"""Base interface shared by the interfaces that write a collection of image files to NWB."""

from pathlib import Path
from typing import Literal

from pynwb import NWBFile
from pynwb.base import BaseImage, Images

from ...basedatainterface import BaseDataInterface
from ...utils import DeepDict, get_base_schema


[docs] class BaseImageInterface(BaseDataInterface): """ Base class for the interfaces that write a collection of image files into an ``Images`` container. Subclasses declare the file suffixes they can write in ``associated_suffixes`` and implement ``_create_nwb_image``, which turns a single file into the NWB image object placed in the container. """
[docs] @classmethod def get_source_schema(cls) -> dict: """Return the schema for the source_data.""" return dict( required=["file_paths"], properties=dict( file_paths=dict( type="array", items=dict(type="string"), description="List of paths to image files to be converted", ), folder_path=dict( type="string", description="Path to folder containing images to be converted. Used if file_paths not provided.", ), ), )
def __init__( self, file_paths: list[str | Path] | None = None, folder_path: str | Path | None = None, *, metadata_key: str = "Images", verbose: bool = True, ): """ Initialize the image interface. Parameters ---------- file_paths : list of str | Path, optional List of paths to image files to be converted folder_path : str | Path, optional Path to folder containing images to be converted. Used if file_paths not provided. metadata_key : str, default: "Images" Key to use in metadata["Images"][metadata_key] for storing container metadata verbose : bool, default: True Whether to print status messages """ if file_paths is None and folder_path is None: raise ValueError("Either file_paths or folder_path must be provided") if file_paths is not None and folder_path is not None: raise ValueError("Only one of file_paths or folder_path should be provided") self.file_paths = file_paths self.folder_path = folder_path self.metadata_key = metadata_key # Destination set at construction through `ImageInterface`'s deprecated `images_location`. # `add_to_nwbfile` falls back to it when its own `parent_container` is not given. self.parent_container = None super().__init__( verbose=verbose, file_paths=file_paths, folder_path=folder_path, metadata_key=metadata_key, ) # Process paths if folder_path is not None: folder = Path(folder_path) if not folder.exists(): raise ValueError(f"Folder path {folder} does not exist") # Get all image files in folder file_paths = [] for suffix in self.associated_suffixes: file_paths.extend(folder.glob(f"*{suffix}")) if not file_paths: raise ValueError(f"No image files found in {folder}") self.file_paths = [Path(p) for p in file_paths] def _get_image_metadata_properties(self) -> dict: """ Return the properties a single image's metadata block accepts. Declared here rather than on the container schema so that a subclass writing an NWB image type with more fields on it, `resolution` on the embedded `Image`, can add them without restating the rest. """ return dict( name=dict(type="string", description="Name of the image in the container."), description=dict(type="string", description="Description of the image."), )
[docs] def get_metadata_schema(self) -> dict: """Return the schema for the metadata, declaring the ``Images`` container and its per-image blocks.""" metadata_schema = super().get_metadata_schema() image_schema = dict( type="object", properties=self._get_image_metadata_properties(), additionalProperties=False, ) container_schema = dict( type="object", properties=dict( name=dict(type="string", description="Name of the Images container."), description=dict(type="string", description="Description of the Images container."), # Keyed by the file path of each image, so the entries are validated rather than the keys. images=dict(type="object", additionalProperties=image_schema), ), additionalProperties=False, ) metadata_schema["properties"]["Images"] = get_base_schema( tag="Images", properties={self.metadata_key: container_schema}, required=[self.metadata_key], additionalProperties=False, ) return metadata_schema
[docs] def get_metadata(self) -> DeepDict: """ Get metadata for the images. This method returns a metadata structure that includes both container-level and per-image metadata. The per-image metadata allows customization of individual image properties such as name, resolution, and description. Returns ------- DeepDict Metadata dictionary with the following structure:: { "Images": { "<metadata_key>": { "name": str, Name of the Images container (defaults to metadata_key) "description": str, Description of the Images container "images": { "<file_path_1>": { "name": str, Name for the individual image (defaults to file stem) "resolution": float, optional Resolution in pixels/cm (can be added by user, embedded images only) "description": str, optional Description of the individual image (can be added by user) }, "<file_path_2>": { ... } } } } } Examples -------- Basic usage: >>> interface = ImageInterface(file_paths=["/data/img1.png", "/data/img2.jpg"]) >>> metadata = interface.get_metadata() >>> print(metadata["Images"]["ImagesRGB"]["images"]) {"/data/img1.png": {"name": "img1"}, "/data/img2.jpg": {"name": "img2"}} Customizing per-image metadata: >>> metadata = interface.get_metadata() >>> metadata["Images"]["ImagesRGB"]["images"]["/data/img1.png"]["resolution"] = 2.5 >>> metadata["Images"]["ImagesRGB"]["images"]["/data/img1.png"]["description"] = "Baseline image" >>> metadata["Images"]["ImagesRGB"]["images"]["/data/img2.jpg"]["name"] = "treatment_image" >>> interface.add_to_nwbfile(nwbfile, metadata=metadata) Notes ----- - The "images" dictionary maps file paths (as strings) to individual image metadata - Users can modify the returned metadata to customize image properties before calling add_to_nwbfile() - Resolution should be specified in pixels/cm if provided - If resolution or description are not specified, they will not be passed to the NWB image objects - Image names default to the file stem but can be overridden in the metadata """ metadata = super().get_metadata() # Add basic metadata about the images under the specified key if "Images" not in metadata: metadata["Images"] = {} # Create images_dict mapping file_path to individual image metadata images_metadata_dict = {} for file_path in self.file_paths: file_path_str = str(file_path) images_metadata_dict[file_path_str] = { "name": Path(file_path).stem, # Default name from file stem # Users can add "resolution" and "description" keys as needed } metadata["Images"][self.metadata_key] = dict( name=self.metadata_key, description=self._get_default_container_description(), images=images_metadata_dict, ) return metadata
def _get_default_container_description(self) -> str: """Return the description used for the ``Images`` container when the metadata does not state one.""" return f"Images loaded through {self.__class__.__name__}" def _create_nwb_image(self, *, file_path: Path, image_metadata: dict) -> BaseImage: """ Build the NWB image object written for a single file. Parameters ---------- file_path : Path Path of the image file to write. image_metadata : dict Per-image metadata block for this file, as returned by `get_metadata`. Returns ------- BaseImage The image object to add to the `Images` container. """ raise NotImplementedError
[docs] def add_to_nwbfile( self, nwbfile: NWBFile, metadata: DeepDict | None = None, *, parent_container: Literal["acquisition", "stimulus"] | None = None, ) -> None: """ Add the image data to an NWB file. Parameters ---------- nwbfile : NWBFile The NWB file to add the images to metadata : dict, optional Metadata for the images parent_container : {"acquisition", "stimulus"}, optional The group of the NWB file the ``Images`` container is written to, "acquisition" by default. """ if parent_container is None: parent_container = self.parent_container or "acquisition" if parent_container not in {"acquisition", "stimulus"}: raise ValueError(f"parent_container must be either 'acquisition' or 'stimulus', not {parent_container}.") if metadata is None: metadata = self.get_metadata() # Get metadata for this specific container images_metadata = metadata.get("Images", {}) container_metadata = images_metadata.get(self.metadata_key, {}) name = container_metadata.get("name", self.metadata_key) description = container_metadata.get("description", self._get_default_container_description()) # Create Images container images_container = Images( name=name, description=description, ) # Process each image images_metadata_dict = container_metadata.get("images", {}) for file_path in self.file_paths: image_metadata = images_metadata_dict.get(str(file_path), {}) nwb_image = self._create_nwb_image(file_path=file_path, image_metadata=image_metadata) # Add to images container images_container.add_image(nwb_image) # Add images container to nwb file if parent_container == "acquisition": nwbfile.add_acquisition(images_container) else: nwbfile.add_stimulus(images_container)