From 3b719399f29ce6d48f87faacd9c62aa1a666d809 Mon Sep 17 00:00:00 2001 From: Jackson Maxfield Brown Date: Fri, 16 Jul 2021 16:36:24 -0700 Subject: [PATCH] feature/ome-metadata-xslt-spec (#289) * Write generic function for XSLT transformation * Add prop def for ome meta for aicsimage and reader * Passthrough impl for OmeTiffReader * Add basic tests for all readers * Add lxml to deps * Bump min ome-types * Change docstring of NotImplError --- aicsimageio/aics_image.py | 18 +++++++ aicsimageio/metadata/utils.py | 66 +++++++++++++++++++++++- aicsimageio/readers/ome_tiff_reader.py | 4 ++ aicsimageio/readers/reader.py | 18 +++++++ aicsimageio/tests/metadata/__init__.py | 2 + aicsimageio/tests/metadata/test_utils.py | 54 +++++++++++++++++++ setup.py | 3 +- 7 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 aicsimageio/tests/metadata/__init__.py create mode 100644 aicsimageio/tests/metadata/test_utils.py diff --git a/aicsimageio/aics_image.py b/aicsimageio/aics_image.py index a697dba0f..b534ae705 100644 --- a/aicsimageio/aics_image.py +++ b/aicsimageio/aics_image.py @@ -8,6 +8,7 @@ import dask.array as da import numpy as np import xarray as xr +from ome_types import OME from . import dimensions, exceptions, transforms, types from .formats import FORMAT_IMPLEMENTATIONS @@ -663,6 +664,23 @@ def metadata(self) -> Any: """ return self.reader.metadata + @property + def ome_metadata(self) -> OME: + """ + Returns + ------- + metadata: OME + The original metadata transformed into the OME specfication. + This likely isn't a complete transformation but is guarenteed to + be a valid transformation. + + Raises + ------ + NotImplementedError + No metadata transformer available. + """ + return self.reader.ome_metadata + @property def channel_names(self) -> List[str]: """ diff --git a/aicsimageio/metadata/utils.py b/aicsimageio/metadata/utils.py index 994049c80..c098d3a75 100644 --- a/aicsimageio/metadata/utils.py +++ b/aicsimageio/metadata/utils.py @@ -2,14 +2,20 @@ # -*- coding: utf-8 -*- import logging +import os import re -import xml.etree.ElementTree as ET from copy import deepcopy +from pathlib import Path from typing import Dict, Optional, Union +from xml.etree import ElementTree as ET +import lxml.etree import numpy as np +from ome_types import OME, from_xml from ome_types.model.simple_types import PixelType +from ..types import PathLike + ############################################################################### log = logging.getLogger(__name__) @@ -36,6 +42,64 @@ ############################################################################### +def transform_metadata_with_xslt( + tree: ET.Element, + xslt: PathLike, +) -> OME: + """ + Given an in-memory metadata Element and a path to an XSLT file, convert + metadata to OME. + + Parameters + ---------- + tree: ET.Element + The metadata tree to convert. + xslt: PathLike + Path to the XSLT file. + + Returns + ------- + ome: OME + The generated / translated OME metadata. + + Notes + ----- + This function will briefly update your processes current working directory + to the directory that stores the XSLT file. + """ + # Store current process directory + process_dir = Path().cwd() + + # Make xslt path absolute + xslt_abs_path = Path(xslt).resolve(strict=True).absolute() + + # Try the transform + try: + # We switch directories so that whatever sub-moduled in XSLT + # main file can have local references to supporting transforms. + # i.e. the main XSLT file imports a transformers for specific sections + # of the metadata (camera, experiment, etc.) + os.chdir(xslt_abs_path.parent) + + # Parse template and generate transform function + template = lxml.etree.parse(str(xslt_abs_path)) + transform = lxml.etree.XSLT(template) + + # Convert from stdlib ET to lxml ET + tree_str = ET.tostring(tree) + lxml_tree = lxml.etree.fromstring(tree_str) + ome_etree = transform(lxml_tree) + + # Dump generated etree to string and read with ome-types + ome = from_xml(str(ome_etree)) + + # Regardless of error or succeed, move back to original process dir + finally: + os.chdir(process_dir) + + return ome + + def generate_ome_image_id(image_id: Union[str, int]) -> str: """ Naively generates the standard OME image ID using a provided ID. diff --git a/aicsimageio/readers/ome_tiff_reader.py b/aicsimageio/readers/ome_tiff_reader.py index 3aa9050eb..8c02229e7 100644 --- a/aicsimageio/readers/ome_tiff_reader.py +++ b/aicsimageio/readers/ome_tiff_reader.py @@ -391,6 +391,10 @@ def _read_immediate(self) -> xr.DataArray: tiff_tags, ) + @property + def ome_metadata(self) -> OME: + return self.metadata + @property def physical_pixel_sizes(self) -> PhysicalPixelSizes: """ diff --git a/aicsimageio/readers/reader.py b/aicsimageio/readers/reader.py index 854151241..1b5937e3c 100644 --- a/aicsimageio/readers/reader.py +++ b/aicsimageio/readers/reader.py @@ -9,6 +9,7 @@ import numpy as np import xarray as xr from fsspec.spec import AbstractFileSystem +from ome_types import OME from .. import constants, exceptions, transforms, types from ..dimensions import DEFAULT_DIMENSION_ORDER, DimensionNames, Dimensions @@ -697,6 +698,23 @@ def metadata(self) -> Any: return self._metadata + @property + def ome_metadata(self) -> OME: + """ + Returns + ------- + metadata: OME + The original metadata transformed into the OME specfication. + This likely isn't a complete transformation but is guarenteed to + be a valid transformation. + + Raises + ------ + NotImplementedError + No metadata transformer available. + """ + raise NotImplementedError() + @property def channel_names(self) -> Optional[List[str]]: """ diff --git a/aicsimageio/tests/metadata/__init__.py b/aicsimageio/tests/metadata/__init__.py new file mode 100644 index 000000000..faa18be5b --- /dev/null +++ b/aicsimageio/tests/metadata/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- diff --git a/aicsimageio/tests/metadata/test_utils.py b/aicsimageio/tests/metadata/test_utils.py new file mode 100644 index 000000000..44b97907f --- /dev/null +++ b/aicsimageio/tests/metadata/test_utils.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import pytest +from ome_types import OME + +from aicsimageio import AICSImage + +from ..conftest import get_resource_full_path, host + +############################################################################### + + +@host +@pytest.mark.parametrize( + "filename", + [ + # DefaultReader + pytest.param( + "example.png", + marks=pytest.mark.raises(execeptions=NotImplementedError), + ), + # TiffReader + pytest.param( + "s_1_t_10_c_3_z_1.tiff", + marks=pytest.mark.raises(execeptions=NotImplementedError), + ), + # OmeTiffReader + ("actk.ome.tiff"), + # LifReader + pytest.param( + "tiled.lif", + marks=pytest.mark.raises(execeptions=NotImplementedError), + ), + # CziReader + pytest.param( + "s_1_t_1_c_1_z_1.czi", + marks=pytest.mark.raises(execeptions=NotImplementedError), + ), + pytest.param( + "RGB-8bit.czi", + marks=pytest.mark.raises(execeptions=NotImplementedError), + ), + ], +) +def test_ome_metadata(filename: str, host: str) -> None: + # Get full filepath + uri = get_resource_full_path(filename, host) + + # Init image + img = AICSImage(uri) + + # Test the transform + assert isinstance(img.ome_metadata, OME) diff --git a/setup.py b/setup.py index 99c5a515c..f8041ba40 100644 --- a/setup.py +++ b/setup.py @@ -68,8 +68,9 @@ "dask[array]>=2021.4.1", "fsspec>=2021.4.0", "imagecodecs>=2020.5.30", + "lxml~=4.6.3", "numpy~=1.16", - "ome-types~=0.2.4", + "ome-types~=0.2.7", "tifffile>=2021.6.6", "toolz~=0.11.0", "xarray~=0.16.1",