Skip to content

Commit

Permalink
Merge pull request #60 from thewtex/hatch
Browse files Browse the repository at this point in the history
ENH: Migrate to hatch for packaging
  • Loading branch information
thewtex authored Sep 19, 2022
2 parents 53ce8c9 + ce4d63a commit b315a88
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 144 deletions.
4 changes: 4 additions & 0 deletions multiscale_spatial_image/__about__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2022-present NumFOCUS <info@numfocus.org>
#
# SPDX-License-Identifier: MIT
__version__ = '0.10.0'
16 changes: 16 additions & 0 deletions multiscale_spatial_image/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""multiscale-spatial-image
Generate a multiscale spatial image."""


__all__ = [
"MultiscaleSpatialImage",
"Methods",
"to_multiscale",
"itk_image_to_multiscale",
"__version__",
]

from .__about__ import __version__
from .multiscale_spatial_image import MultiscaleSpatialImage, Methods
from .to_multiscale import to_multiscale, itk_image_to_multiscale
129 changes: 129 additions & 0 deletions multiscale_spatial_image/multiscale_spatial_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from typing import Union, List
from enum import Enum

import xarray as xr
from datatree import DataTree
from datatree.treenode import TreeNode
import numpy as np


class MultiscaleSpatialImage(DataTree):
"""A multi-scale representation of a spatial image.
This is an xarray DataTree, with content compatible with the Open Microscopy Environment-
Next Generation File Format (OME-NGFF).
The tree contains nodes in the form: `scale{scale}` where *scale* is the integer scale.
Each node has a the same named `Dataset` that corresponds to to the NGFF dataset name.
For example, a three-scale representation of a *cells* dataset would have `Dataset` nodes:
scale0
scale1
scale2
"""

def __init__(
self,
name: str = "multiscales",
data: Union[xr.Dataset, xr.DataArray] = None,
parent: TreeNode = None,
children: List[TreeNode] = None,
):
"""DataTree with a root name of *multiscales*."""
super().__init__(data=data, name=name, parent=parent, children=children)

def to_zarr(self, store, mode: str = "w", encoding=None, **kwargs):
"""
Write multi-scale spatial image contents to a Zarr store.
Metadata is added according the OME-NGFF standard.
store : MutableMapping, str or Path, optional
Store or path to directory in file system
mode : {{"w", "w-", "a", "r+", None}, default: "w"
Persistence mode: “w” means create (overwrite if exists); “w-” means create (fail if exists);
“a” means override existing variables (create if does not exist); “r+” means modify existing
array values only (raise an error if any metadata or shapes would change). The default mode
is “a” if append_dim is set. Otherwise, it is “r+” if region is set and w- otherwise.
encoding : dict, optional
Nested dictionary with variable names as keys and dictionaries of
variable specific encodings as values, e.g.,
``{"scale0/image": {"my_variable": {"dtype": "int16", "scale_factor": 0.1}, ...}, ...}``.
See ``xarray.Dataset.to_zarr`` for available options.
kwargs :
Additional keyword arguments to be passed to ``datatree.DataTree.to_zarr``
"""

multiscales = []
scale0 = self[self.groups[1]]
for name in scale0.ds.data_vars.keys():

ngff_datasets = []
for child in self.children:
image = self[child].ds
scale_transform = []
translate_transform = []
for dim in image.dims:
if len(image.coords[dim]) > 1 and np.issubdtype(image.coords[dim].dtype, np.number):
scale_transform.append(
float(image.coords[dim][1] - image.coords[dim][0])
)
else:
scale_transform.append(1.0)
if len(image.coords[dim]) > 0 and np.issubdtype(image.coords[dim].dtype, np.number):
translate_transform.append(float(image.coords[dim][0]))
else:
translate_transform.append(0.0)

ngff_datasets.append(
{
"path": f"{self[child].name}/{name}",
"coordinateTransformations": [
{
"type": "scale",
"scale": scale_transform,
},
{
"type": "translation",
"translation": translate_transform,
},
],
}
)

image = scale0.ds
axes = []
for axis in image.dims:
if axis == "t":
axes.append({"name": "t", "type": "time"})
elif axis == "c":
axes.append({"name": "c", "type": "channel"})
else:
axes.append({"name": axis, "type": "space"})
if "units" in image.coords[axis].attrs:
axes[-1]["unit"] = image.coords[axis].attrs["units"]

multiscales.append(
{
"@type": "ngff:Image",
"version": "0.4",
"name": name,
"axes": axes,
"datasets": ngff_datasets,
}
)

# NGFF v0.4 metadata
ngff_metadata = {"multiscales": multiscales, "multiscaleSpatialImageVersion": 1}
self.ds = self.ds.assign_attrs(**ngff_metadata)

super().to_zarr(store, **kwargs)


class Methods(Enum):
XARRAY_COARSEN = "xarray.DataArray.coarsen"
ITK_BIN_SHRINK = "itk.bin_shrink_image_filter"
ITK_GAUSSIAN = "itk.discrete_gaussian_image_filter"
ITK_LABEL_GAUSSIAN = "itk.discrete_gaussian_image_filter_label_interpolator"
DASK_IMAGE_GAUSSIAN = "dask_image.ndfilters.gaussian_filter"

Original file line number Diff line number Diff line change
@@ -1,144 +1,13 @@
"""multiscale-spatial-image
Generate a multiscale spatial image."""

__version__ = "0.9.0"

import enum
from typing import Union, Sequence, List, Optional, Dict, Mapping, Any, Tuple
from enum import Enum

from spatial_image import to_spatial_image, SpatialImage # type: ignore

import xarray as xr
from dask.array import map_blocks, map_overlap
from datatree import DataTree
from datatree.treenode import TreeNode
import numpy as np

_spatial_dims = {"x", "y", "z"}


class MultiscaleSpatialImage(DataTree):
"""A multi-scale representation of a spatial image.
This is an xarray DataTree, with content compatible with the Open Microscopy Environment-
Next Generation File Format (OME-NGFF).
The tree contains nodes in the form: `scale{scale}` where *scale* is the integer scale.
Each node has a the same named `Dataset` that corresponds to to the NGFF dataset name.
For example, a three-scale representation of a *cells* dataset would have `Dataset` nodes:
scale0
scale1
scale2
"""

def __init__(
self,
name: str = "multiscales",
data: Union[xr.Dataset, xr.DataArray] = None,
parent: TreeNode = None,
children: List[TreeNode] = None,
):
"""DataTree with a root name of *multiscales*."""
super().__init__(data=data, name=name, parent=parent, children=children)

def to_zarr(self, store, mode: str = "w", encoding=None, **kwargs):
"""
Write multi-scale spatial image contents to a Zarr store.
Metadata is added according the OME-NGFF standard.
store : MutableMapping, str or Path, optional
Store or path to directory in file system
mode : {{"w", "w-", "a", "r+", None}, default: "w"
Persistence mode: “w” means create (overwrite if exists); “w-” means create (fail if exists);
“a” means override existing variables (create if does not exist); “r+” means modify existing
array values only (raise an error if any metadata or shapes would change). The default mode
is “a” if append_dim is set. Otherwise, it is “r+” if region is set and w- otherwise.
encoding : dict, optional
Nested dictionary with variable names as keys and dictionaries of
variable specific encodings as values, e.g.,
``{"scale0/image": {"my_variable": {"dtype": "int16", "scale_factor": 0.1}, ...}, ...}``.
See ``xarray.Dataset.to_zarr`` for available options.
kwargs :
Additional keyword arguments to be passed to ``datatree.DataTree.to_zarr``
"""

multiscales = []
scale0 = self[self.groups[1]]
for name in scale0.ds.data_vars.keys():

ngff_datasets = []
for child in self.children:
image = self[child].ds
scale_transform = []
translate_transform = []
for dim in image.dims:
if len(image.coords[dim]) > 1 and np.issubdtype(image.coords[dim].dtype, np.number):
scale_transform.append(
float(image.coords[dim][1] - image.coords[dim][0])
)
else:
scale_transform.append(1.0)
if len(image.coords[dim]) > 0 and np.issubdtype(image.coords[dim].dtype, np.number):
translate_transform.append(float(image.coords[dim][0]))
else:
translate_transform.append(0.0)

ngff_datasets.append(
{
"path": f"{self[child].name}/{name}",
"coordinateTransformations": [
{
"type": "scale",
"scale": scale_transform,
},
{
"type": "translation",
"translation": translate_transform,
},
],
}
)

image = scale0.ds
axes = []
for axis in image.dims:
if axis == "t":
axes.append({"name": "t", "type": "time"})
elif axis == "c":
axes.append({"name": "c", "type": "channel"})
else:
axes.append({"name": axis, "type": "space"})
if "units" in image.coords[axis].attrs:
axes[-1]["unit"] = image.coords[axis].attrs["units"]

multiscales.append(
{
"@type": "ngff:Image",
"version": "0.4",
"name": name,
"axes": axes,
"datasets": ngff_datasets,
}
)

# NGFF v0.4 metadata
ngff_metadata = {"multiscales": multiscales, "multiscaleSpatialImageVersion": 1}
self.ds = self.ds.assign_attrs(**ngff_metadata)

super().to_zarr(store, **kwargs)


class Methods(Enum):
XARRAY_COARSEN = "xarray.DataArray.coarsen"
ITK_BIN_SHRINK = "itk.bin_shrink_image_filter"
ITK_GAUSSIAN = "itk.discrete_gaussian_image_filter"
ITK_LABEL_GAUSSIAN = "itk.discrete_gaussian_image_filter_label_interpolator"
DASK_IMAGE_GAUSSIAN = "dask_image.ndfilters.gaussian_filter"

from . import MultiscaleSpatialImage, Methods

def to_multiscale(
image: SpatialImage,
Expand Down Expand Up @@ -861,4 +730,4 @@ def itk_image_to_multiscale(
return to_multiscale(spatial_image,
scale_factors,
method=method,
chunks=chunks)
chunks=chunks)
50 changes: 40 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
[build-system]
requires = ["flit_core >=2,<4"]
build-backend = "flit_core.buildapi"
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.flit.metadata]
module = "multiscale_spatial_image"
author = "Matt McCormick"
author-email = "matt.mccormick@kitware.com"
[project]
name = "multiscale_spatial_image"
description = "Generate a multiscale, chunked, multi-dimensional spatial image data structure that can be serialized to OME-NGFF."
authors = [{name = "Matt McCormick", email = "matt.mccormick@kitware.com"}]
readme = "README.md"
license = {file = "LICENSE"}
home-page = "https://github.com/spatial-image/multiscale-spatial-image"
classifiers = [ "License :: OSI Approved :: Apache Software License",]
description-file = "README.md"
classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
'Intended Audience :: Developers',
'Intended Audience :: Science/Research',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
]
keywords = [
"itk",
"ngff",
"ome",
"zarr",
"dask",
"imaging",
"visualization",
]
dynamic = ["version"]

requires = [
requires-python = ">=3.8"
dependencies = [
"numpy",
"dask",
"xarray",
"xarray-datatree>=0.0.5",
"spatial_image>=0.2.1",
]

[tool.flit.metadata.requires-extra]
[project.urls]
Home = "https://github.com/spatial-image/multiscale-spatial-image"
Source = "https://github.com/spatial-image/multiscale-spatial-image"
Issues = "https://github.com/spatial-image/multiscale-spatial-image"

[project.optional-dependencies]
test = [
"itk-filtering>=5.3rc4",
"dask_image",
Expand All @@ -35,3 +62,6 @@ test = [

[tool.black]
line-length = 88

[tool.hatch.version]
path = "multiscale_spatial_image/__about__.py"
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from platform import processor
import pytest

from pathlib import Path
Expand Down

0 comments on commit b315a88

Please sign in to comment.