Skip to content

Commit

Permalink
feature/ome-metadata-xslt-spec (#289)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Jackson Maxfield Brown authored Jul 16, 2021
1 parent fee46ad commit 3b71939
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 2 deletions.
18 changes: 18 additions & 0 deletions aicsimageio/aics_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
"""
Expand Down
66 changes: 65 additions & 1 deletion aicsimageio/metadata/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions aicsimageio/readers/ome_tiff_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
18 changes: 18 additions & 0 deletions aicsimageio/readers/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]]:
"""
Expand Down
2 changes: 2 additions & 0 deletions aicsimageio/tests/metadata/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
54 changes: 54 additions & 0 deletions aicsimageio/tests/metadata/test_utils.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 3b71939

Please sign in to comment.