From 728619cd9b129ebf1239354549c2ac0e2beaea6d Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 27 Jan 2023 12:53:23 -0600 Subject: [PATCH 01/41] Add Linear and Enumerated axis classes to jc --- src/imagej/_java.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/imagej/_java.py b/src/imagej/_java.py index 1de17a29..0083b370 100644 --- a/src/imagej/_java.py +++ b/src/imagej/_java.py @@ -50,6 +50,14 @@ def MetadataWrapper(self): def LabelingIOService(self): return "io.scif.labeling.LabelingIOService" + @JavaClasses.java_import + def DefaultLinearAxis(self): + return "net.imagej.axis.DefaultLinearAxis" + + @JavaClasses.java_import + def EnumeratedAxis(self): + return "net.imagej.axis.EnumeratedAxis" + @JavaClasses.java_import def Dataset(self): return "net.imagej.Dataset" From 53d23eaf38bd4939727ad085ccfe3350be798282 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 30 Jan 2023 13:22:46 -0600 Subject: [PATCH 02/41] Add _create_image_metadata function This function creates a metadata dictionary meant to be stored in a newly created xarray.DataArray's global attributes. The initial metadata created stores scale type of the axis (linear, enumerated or none). --- src/imagej/convert.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/imagej/convert.py b/src/imagej/convert.py index a3bcdb89..896e463d 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -4,7 +4,7 @@ import ctypes import logging import os -from typing import Dict, Sequence +from typing import Dict, Sequence, Union import imglyb import numpy as np @@ -232,6 +232,7 @@ def java_to_xarray(ij: "jc.ImageJ", jobj) -> xr.DataArray: xr_dims = list(permuted_rai.dims) xr_attrs = sj.to_python(permuted_rai.getProperties()) xr_attrs = {sj.to_python(k): sj.to_python(v) for k, v in xr_attrs.items()} + xr_attrs["imagej"] = _create_imagej_metadata(xr_axes, xr_dims) # reverse axes and dims to match narr xr_axes.reverse() xr_dims.reverse() @@ -562,6 +563,28 @@ def _rename_xarray_dims(xarr, new_dims: Sequence[str]): return xarr.rename(dim_map) +def _create_imagej_metadata( + axes: Sequence[Union["jc.DefaultLinearAxis", "jc.EnumeratedAxis"]], + dim_seq: Sequence[str], +) -> dict: + """ + Create the ImageJ metadata attribute dictionary for xarray's global attributes. + """ + ij_metadata = {} + + # store axis scale type + assert len(axes) == len(dim_seq) + for i in range(len(axes)): + if isinstance(axes[i], jc.DefaultLinearAxis): + ij_metadata[dims._to_ijdim(dim_seq[i]) + "_axis_scale"] = "linear" + elif isinstance(axes[i], jc.EnumeratedAxis): + ij_metadata[dims._to_ijdim(dim_seq[i]) + "_axis_scale"] = "enumerated" + else: + ij_metadata[dims._to_ijdim(dim_seq[i]) + "_axis_scale"] = None + + return ij_metadata + + def _delete_labeling_files(filepath): """ Removes any Labeling data left over at filepath From d5375dc222e0e5a911e6d2ba4b40d7991d1de698 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 30 Jan 2023 14:16:10 -0600 Subject: [PATCH 03/41] Update _assign_axes to use "imagej" metadata This commit changes how the linear and enumerated axes are assigned. We now look for the "imagej" key in the xarray's global attributes. If the key is present we look for dim + "_axis_scale" to assign linear or enumerated axes. --- src/imagej/_java.py | 4 +++ src/imagej/dims.py | 74 +++++++++++++++++++++++++++------------------ 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/imagej/_java.py b/src/imagej/_java.py index 0083b370..6b4e8bf6 100644 --- a/src/imagej/_java.py +++ b/src/imagej/_java.py @@ -30,6 +30,10 @@ class MyJavaClasses(JavaClasses): significantly easier and more readable. """ + @JavaClasses.java_import + def Double(self): + return "java.lang.Double" + @JavaClasses.java_import def Throwable(self): return "java.lang.Throwable" diff --git a/src/imagej/dims.py b/src/imagej/dims.py index f009c3a0..1877cc07 100644 --- a/src/imagej/dims.py +++ b/src/imagej/dims.py @@ -2,7 +2,7 @@ Utility functions for querying and manipulating dimensional axis metadata. """ import logging -from typing import List, Tuple +from typing import List, Tuple, Union import numpy as np import scyjava as sj @@ -177,49 +177,55 @@ def prioritize_rai_axes_order( return permute_order -def _assign_axes(xarr: xr.DataArray): +def _assign_axes( + xarr: xr.DataArray, +) -> List[Union["jc.DefaultLinearAxis", "jc.EnumeratedAxis"]]: """ - Obtain xarray axes names, origin, and scale and convert into ImageJ Axis; - currently supports EnumeratedAxis - :param xarr: xarray that holds the units - :return: A list of ImageJ Axis with the specified origin and scale + Obtain xarray axes names, origin, scale and convert into ImageJ Axis. Supports both + DefaultLinearAxis and the newer EnumeratedAxis. + :param xarr: xarray that holds the data. + :return: A list of ImageJ Axis with the specified origin and scale. """ - Double = sj.jimport("java.lang.Double") - - axes = [""] * len(xarr.dims) - - # try to get EnumeratedAxis, if not then default to LinearAxis in the loop - try: - EnumeratedAxis = _get_enumerated_axis() - except (JException, TypeError): - EnumeratedAxis = None - + axes = [""] * xarr.ndim for dim in xarr.dims: - axis_str = _convert_dim(dim, direction="java") + axis_str = _convert_dim(dim, "java") ax_type = jc.Axes.get(axis_str) ax_num = _get_axis_num(xarr, dim) - scale = _get_scale(xarr.coords[dim]) + coords_arr = xarr.coords[dim].to_numpy() - if scale is None: + # check if coords/scale is numeric + if _is_numeric_scale(coords_arr): + doub_coords = [jc.Double(np.double(x)) for x in xarr.coords[dim]] + else: _logger.warning( f"The {ax_type.label} axis is non-numeric and is translated " "to a linear index." ) doub_coords = [ - Double(np.double(x)) for x in np.arange(len(xarr.coords[dim])) + jc.Double(np.double(x)) for x in np.arrange(len(xarr.coords[dim])) ] - else: - doub_coords = [Double(np.double(x)) for x in xarr.coords[dim]] - # EnumeratedAxis is a new axis made for xarray, so is only present in - # ImageJ versions that are released later than March 2020. - # This actually returns a LinearAxis if using an earlier version. - if EnumeratedAxis is not None: - java_axis = EnumeratedAxis(ax_type, sj.to_java(doub_coords)) + # assign axis scale type -- checks for imagej metadata + if "imagej" in xarr.attrs.keys(): + ij_dim = _convert_dim(dim, "java") + if ij_dim + "_axis_scale" in xarr.attrs["imagej"].keys(): + scale_type = xarr.attrs["imagej"][ij_dim + "_axis_scale"] + if scale_type == "linear": + jaxis = _get_linear_axis(ax_type, sj.to_java(doub_coords)) + if scale_type == "enumerated": + try: + EnumeratedAxis = _get_enumerated_axis() + except (JException, TypeError): + EnumeratedAxis = None + if EnumeratedAxis is not None: + jaxis = EnumeratedAxis(ax_type, sj.to_java(doub_coords)) + else: + jaxis = _get_linear_axis(ax_type, sj.to_java(doub_coords)) else: - java_axis = _get_linear_axis(ax_type, sj.to_java(doub_coords)) + # default to DefaultLinearAxis always if no `scale_type` key in attr + jaxis = _get_linear_axis(ax_type, sj.to_java(doub_coords)) - axes[ax_num] = java_axis + axes[ax_num] = jaxis return axes @@ -295,6 +301,16 @@ def _get_scale(axis): return None +def _is_numeric_scale(coords_array: np.ndarray) -> bool: + """ + Checks if the coordinates array of the given axis is numeric. + + :param coords_array: A 1D NumPy array. + :return: bool + """ + return np.issubdtype(coords_array.dtype, np.number) + + def _get_enumerated_axis(): """Get EnumeratedAxis. From 9adf026839641e3059884fb196af94d7f926f5d0 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 31 Jan 2023 12:36:46 -0600 Subject: [PATCH 04/41] Add metadata for all CalibratedAxis types Although the only calibrated axes used by nearly everyone are DefaultLinearAxis and EnumeratedAxis, I added metadata support for all CalibratedAxis types (e.g. PolynomialAxis etc...) just to be thorough. This metadata will be used for matching the Calibrated Axis type when going back to ImageJ/Java land. --- src/imagej/_java.py | 48 +++++++++++++++++++++++++++++++++++++++++++ src/imagej/convert.py | 16 ++++++++------- src/imagej/dims.py | 33 ++++++++++++++++++++++++++--- 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/src/imagej/_java.py b/src/imagej/_java.py index 6b4e8bf6..dd64798d 100644 --- a/src/imagej/_java.py +++ b/src/imagej/_java.py @@ -54,6 +54,10 @@ def MetadataWrapper(self): def LabelingIOService(self): return "io.scif.labeling.LabelingIOService" + @JavaClasses.java_import + def ChapmanRichardsAxis(self): + return "net.imagej.axis.ChapmanRichardsAxis" + @JavaClasses.java_import def DefaultLinearAxis(self): return "net.imagej.axis.DefaultLinearAxis" @@ -62,6 +66,50 @@ def DefaultLinearAxis(self): def EnumeratedAxis(self): return "net.imagej.axis.EnumeratedAxis" + @JavaClasses.java_import + def ExponentialAxis(self): + return "net.imagej.axis.ExponentialAxis" + + @JavaClasses.java_import + def ExponentialRecoveryAxis(self): + return "net.imagej.axis.ExponentialRecoveryAxis" + + @JavaClasses.java_import + def GammaVariateAxis(self): + return "net.imagej.axis.GammaVariateAxis" + + @JavaClasses.java_import + def GaussianAxis(self): + return "net.imagej.axis.GaussianAxis" + + @JavaClasses.java_import + def IdentityAxis(self): + return "net.imagej.axis.IdentityAxis" + + @JavaClasses.java_import + def InverseRodbardAxis(self): + return "net.imagej.axis.InverseRodbardAxis" + + @JavaClasses.java_import + def LogLinearAxis(self): + return "net.imagej.axis.LogLinearAxis" + + @JavaClasses.java_import + def PolynomialAxis(self): + return "net.imagej.axis.PolynomialAxis" + + @JavaClasses.java_import + def PowerAxis(self): + return "net.imagej.axis.PowerAxis" + + @JavaClasses.java_import + def RodbardAxis(self): + return "net.imagej.axis.RodbardAxis" + + @JavaClasses.java_import + def VariableAxis(self): + return "net.iamgej.axis.VariableAxis" + @JavaClasses.java_import def Dataset(self): return "net.imagej.Dataset" diff --git a/src/imagej/convert.py b/src/imagej/convert.py index 896e463d..a2a4a73e 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -571,16 +571,18 @@ def _create_imagej_metadata( Create the ImageJ metadata attribute dictionary for xarray's global attributes. """ ij_metadata = {} - - # store axis scale type assert len(axes) == len(dim_seq) for i in range(len(axes)): + # get CalibratedAxis type as string (e.g. "EnumeratedAxis") + ij_metadata[ + dims._to_ijdim(dim_seq[i]) + "_cal_axis_type" + ] = dims._cal_axis_type_to_str(axes[i]) + # get scale and origin for DefaultLinearAxis if isinstance(axes[i], jc.DefaultLinearAxis): - ij_metadata[dims._to_ijdim(dim_seq[i]) + "_axis_scale"] = "linear" - elif isinstance(axes[i], jc.EnumeratedAxis): - ij_metadata[dims._to_ijdim(dim_seq[i]) + "_axis_scale"] = "enumerated" - else: - ij_metadata[dims._to_ijdim(dim_seq[i]) + "_axis_scale"] = None + ij_metadata[dims._to_ijdim(dim_seq[i]) + "_scale"] = float(axes[i].scale()) + ij_metadata[dims._to_ijdim(dim_seq[i]) + "_origin"] = float( + axes[i].origin() + ) return ij_metadata diff --git a/src/imagej/dims.py b/src/imagej/dims.py index 1877cc07..722a3743 100644 --- a/src/imagej/dims.py +++ b/src/imagej/dims.py @@ -205,11 +205,11 @@ def _assign_axes( jc.Double(np.double(x)) for x in np.arrange(len(xarr.coords[dim])) ] - # assign axis scale type -- checks for imagej metadata + # assign calibrated axis type -- checks for imagej metadata if "imagej" in xarr.attrs.keys(): ij_dim = _convert_dim(dim, "java") - if ij_dim + "_axis_scale" in xarr.attrs["imagej"].keys(): - scale_type = xarr.attrs["imagej"][ij_dim + "_axis_scale"] + if ij_dim + "_cal_axis_type" in xarr.attrs["imagej"].keys(): + scale_type = xarr.attrs["imagej"][ij_dim + "_cal_axis_type"] if scale_type == "linear": jaxis = _get_linear_axis(ax_type, sj.to_java(doub_coords)) if scale_type == "enumerated": @@ -483,3 +483,30 @@ def _to_ijdim(key: str) -> str: return ijdims[key] else: return key + + +def _cal_axis_type_to_str(key) -> str: + """ + Convert a CalibratedAxis type (e.g. net.imagej.axis.DefaultLinearAxis) to + a string. + """ + cal_axis_types = { + jc.ChapmanRichardsAxis: "ChapmanRichardsAxis", + jc.DefaultLinearAxis: "DefaultLinearAxis", + jc.EnumeratedAxis: "EnumeratedAxis", + jc.ExponentialAxis: "ExponentialAxis", + jc.ExponentialRecoveryAxis: "ExponentialRecoveryAxis", + jc.GammaVariateAxis: "GammaVariateAxis", + jc.GaussianAxis: "GaussianAxis", + jc.IdentityAxis: "IdentityAxis", + jc.InverseRodbardAxis: "InverseRodbardAxis", + jc.LogLinearAxis: "LogLinearAxis", + jc.PolynomialAxis: "PolynomialAxis", + jc.PowerAxis: "PowerAxis", + jc.RodbardAxis: "RodbardAxis", + } + + if key.__class__ in cal_axis_types: + return cal_axis_types[key.__class__] + else: + return "unknown" From ed8f794494c8ff235630120d4142ea480a98b525 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 31 Jan 2023 13:43:24 -0600 Subject: [PATCH 05/41] Refactor dims._assign_axes() This commit refactors dims._assign_axes() to use imagej specific metadata attached to a given xarray.DataArray. If the "imagej" attribute is present in the xarray's global attributes then information like scale, origin and the type of CalibratedAxis (e.g. DefaultLinearAxis, EnumeratedAxis, etc...) can be used to assign the correct calibrated axis per given dimension during the net.imagej.Dataset conversion process. If the desired calibrated axis is not available or the axis is unknown then we fall back to a DefaultLinearAxis and attempt to get scale/origin data from the coordinates. --- src/imagej/convert.py | 2 +- src/imagej/dims.py | 81 +++++++++++++++++++++++++++++++++---------- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/imagej/convert.py b/src/imagej/convert.py index a2a4a73e..1a73001f 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -576,7 +576,7 @@ def _create_imagej_metadata( # get CalibratedAxis type as string (e.g. "EnumeratedAxis") ij_metadata[ dims._to_ijdim(dim_seq[i]) + "_cal_axis_type" - ] = dims._cal_axis_type_to_str(axes[i]) + ] = dims._cal_axis_to_str(axes[i]) # get scale and origin for DefaultLinearAxis if isinstance(axes[i], jc.DefaultLinearAxis): ij_metadata[dims._to_ijdim(dim_seq[i]) + "_scale"] = float(axes[i].scale()) diff --git a/src/imagej/dims.py b/src/imagej/dims.py index 722a3743..f3b0f997 100644 --- a/src/imagej/dims.py +++ b/src/imagej/dims.py @@ -205,25 +205,32 @@ def _assign_axes( jc.Double(np.double(x)) for x in np.arrange(len(xarr.coords[dim])) ] - # assign calibrated axis type -- checks for imagej metadata + # assign calibrated axis type -- checks xarray for imagej metadata + jaxis = None if "imagej" in xarr.attrs.keys(): ij_dim = _convert_dim(dim, "java") if ij_dim + "_cal_axis_type" in xarr.attrs["imagej"].keys(): - scale_type = xarr.attrs["imagej"][ij_dim + "_cal_axis_type"] - if scale_type == "linear": - jaxis = _get_linear_axis(ax_type, sj.to_java(doub_coords)) - if scale_type == "enumerated": + cal_axis_type = xarr.attrs["imagej"][ij_dim + "_cal_axis_type"] + # get scale from metadata if axis type is DefaultLinearAxis + if cal_axis_type == "DefaultLinearAxis": + origin = xarr.attrs["imagej"][ij_dim + "_origin"] + scale = xarr.attrs["imagej"][ij_dim + "_scale"] + jaxis = _str_to_cal_axis(cal_axis_type)( + ax_type, scale, origin + ) # EE: Might need to case scale and origin as jc.Double + else: try: - EnumeratedAxis = _get_enumerated_axis() + jaxis = _str_to_cal_axis(cal_axis_type)( + ax_type, sj.to_java(doub_coords) + ) except (JException, TypeError): - EnumeratedAxis = None - if EnumeratedAxis is not None: - jaxis = EnumeratedAxis(ax_type, sj.to_java(doub_coords)) - else: - jaxis = _get_linear_axis(ax_type, sj.to_java(doub_coords)) + jaxis = _get_fallback_linear_axis( + ax_type, sj.to_java(doub_coords) + ) + else: + jaxis = _get_fallback_linear_axis(ax_type, sj.to_java(doub_coords)) else: - # default to DefaultLinearAxis always if no `scale_type` key in attr - jaxis = _get_linear_axis(ax_type, sj.to_java(doub_coords)) + jaxis = _get_fallback_linear_axis(ax_type, sj.to_java(doub_coords)) axes[ax_num] = jaxis @@ -321,6 +328,18 @@ def _get_enumerated_axis(): return sj.jimport("net.imagej.axis.EnumeratedAxis") +def _get_fallback_linear_axis(axis_type: "jc.AxisType", values): + """ + Get a DefaultLinearAxis manually when all other axes + resources are unavailable. + """ + origin = values[0] + scale = ( + values[1] - values[0] + ) # TODO: replace with _compute_scale() in dim-order-kwargs-test branch + return jc.DefaultLinearAxis(axis_type, scale, origin) + + def _get_linear_axis(axis_type: "jc.AxisType", values): """Get linear axis. @@ -485,12 +504,12 @@ def _to_ijdim(key: str) -> str: return key -def _cal_axis_type_to_str(key) -> str: +def _cal_axis_to_str(key) -> str: """ - Convert a CalibratedAxis type (e.g. net.imagej.axis.DefaultLinearAxis) to + Convert a CalibratedAxis class (e.g. net.imagej.axis.DefaultLinearAxis) to a string. """ - cal_axis_types = { + cal_axis_to_str = { jc.ChapmanRichardsAxis: "ChapmanRichardsAxis", jc.DefaultLinearAxis: "DefaultLinearAxis", jc.EnumeratedAxis: "EnumeratedAxis", @@ -506,7 +525,33 @@ def _cal_axis_type_to_str(key) -> str: jc.RodbardAxis: "RodbardAxis", } - if key.__class__ in cal_axis_types: - return cal_axis_types[key.__class__] + if key.__class__ in cal_axis_to_str: + return cal_axis_to_str[key.__class__] else: return "unknown" + + +def _str_to_cal_axis(key: str): + """ + Convert a string (e.g. "DefaultLinearAxis") to a CalibratedAxis class. + """ + str_to_cal_axis = { + "ChapmanRichardsAxis": jc.ChapmanRichardsAxis, + "DefaultLinearAxis": jc.DefaultLinearAxis, + "EnumeratedAxis": jc.EnumeratedAxis, + "ExponentialAxis": jc.ExponentialAxis, + "ExponentialRecoveryAxis": jc.ExponentialRecoveryAxis, + "GammaVariateAxis": jc.GammaVariateAxis, + "GaussianAxis": jc.GaussianAxis, + "IdentityAxis": jc.IdentityAxis, + "InverseRodbardAxis": jc.InverseRodbardAxis, + "LogLinearAxis": jc.LogLinearAxis, + "PolynomialAxis": jc.PolynomialAxis, + "PowerAxis": jc.PowerAxis, + "RodbardAxis": jc.RodbardAxis, + } + + if key in str_to_cal_axis: + return str_to_cal_axis[key] + else: + return None From 719fe257755c98d9e509e783790bdc7a36e46bf7 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 31 Jan 2023 13:49:28 -0600 Subject: [PATCH 06/41] Remove deprecated Linear and Enumerated axis func --- src/imagej/dims.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/imagej/dims.py b/src/imagej/dims.py index f3b0f997..4b867162 100644 --- a/src/imagej/dims.py +++ b/src/imagej/dims.py @@ -318,16 +318,6 @@ def _is_numeric_scale(coords_array: np.ndarray) -> bool: return np.issubdtype(coords_array.dtype, np.number) -def _get_enumerated_axis(): - """Get EnumeratedAxis. - - EnumeratedAxis is only in releases later than March 2020. If using - an older version of ImageJ without EnumeratedAxis, use - _get_linear_axis() instead. - """ - return sj.jimport("net.imagej.axis.EnumeratedAxis") - - def _get_fallback_linear_axis(axis_type: "jc.AxisType", values): """ Get a DefaultLinearAxis manually when all other axes @@ -340,19 +330,6 @@ def _get_fallback_linear_axis(axis_type: "jc.AxisType", values): return jc.DefaultLinearAxis(axis_type, scale, origin) -def _get_linear_axis(axis_type: "jc.AxisType", values): - """Get linear axis. - - This is used if no EnumeratedAxis is found. If EnumeratedAxis - is available, use _get_enumerated_axis() instead. - """ - DefaultLinearAxis = sj.jimport("net.imagej.axis.DefaultLinearAxis") - origin = values[0] - scale = values[1] - values[0] - axis = DefaultLinearAxis(axis_type, scale, origin) - return axis - - def _dataset_to_imgplus(rai: "jc.RandomAccessibleInterval") -> "jc.ImgPlus": """Get an ImgPlus from a Dataset. From 1c0f3859b37865af641d7cc9bc7c4d4d0234fc24 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 31 Jan 2023 14:23:55 -0600 Subject: [PATCH 07/41] Check for "Hello" key in attributes The new "imagej" metadata attribute may not always be present (depending on the origin of the xarray). Checking for all attributes makes this kind of hard, so lets just check for the key we put in! --- tests/test_image_conversion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_image_conversion.py b/tests/test_image_conversion.py index 977ce47f..562a6d31 100644 --- a/tests/test_image_conversion.py +++ b/tests/test_image_conversion.py @@ -115,7 +115,10 @@ def assert_inverted_xarr_equal_to_xarr(dataset, ij_fixture, xarr): assert list(xarr.dims) == list(invert_xarr.dims) for key in xarr.coords: assert (xarr.coords[key] == invert_xarr.coords[key]).all() - assert xarr.attrs == invert_xarr.attrs + if "Hello" in xarr.attrs.keys(): + assert xarr.attrs["Hello"] == invert_xarr.attrs["Hello"] + else: + assert xarr.attrs == invert_xarr.attrs assert xarr.name == invert_xarr.name From ab2238cf0fe7ddb03f3e07ffc27b6a40f132014b Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Sat, 4 Feb 2023 11:31:59 -0600 Subject: [PATCH 08/41] Add case for singleton coordinates array If a singleton dimension is detected, assign a scale/slope of 1. --- src/imagej/dims.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/imagej/dims.py b/src/imagej/dims.py index 4b867162..1a6bc9cc 100644 --- a/src/imagej/dims.py +++ b/src/imagej/dims.py @@ -324,9 +324,11 @@ def _get_fallback_linear_axis(axis_type: "jc.AxisType", values): resources are unavailable. """ origin = values[0] - scale = ( - values[1] - values[0] - ) # TODO: replace with _compute_scale() in dim-order-kwargs-test branch + # calculate the slope using the values/coord array + if len(values) <= 1: + scale = 1 + else: + scale = values[1] - values[0] return jc.DefaultLinearAxis(axis_type, scale, origin) From 8cf19e5e3865b96e9097b85ddd4e8f0604155e01 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Sat, 4 Feb 2023 11:40:54 -0600 Subject: [PATCH 09/41] Remove unnecessary to_java() call on coords --- src/imagej/dims.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/imagej/dims.py b/src/imagej/dims.py index 1a6bc9cc..b989f3ee 100644 --- a/src/imagej/dims.py +++ b/src/imagej/dims.py @@ -215,22 +215,16 @@ def _assign_axes( if cal_axis_type == "DefaultLinearAxis": origin = xarr.attrs["imagej"][ij_dim + "_origin"] scale = xarr.attrs["imagej"][ij_dim + "_scale"] - jaxis = _str_to_cal_axis(cal_axis_type)( - ax_type, scale, origin - ) # EE: Might need to case scale and origin as jc.Double + jaxis = _str_to_cal_axis(cal_axis_type)(ax_type, scale, origin) else: try: - jaxis = _str_to_cal_axis(cal_axis_type)( - ax_type, sj.to_java(doub_coords) - ) + jaxis = _str_to_cal_axis(cal_axis_type)(ax_type, doub_coords) except (JException, TypeError): - jaxis = _get_fallback_linear_axis( - ax_type, sj.to_java(doub_coords) - ) + jaxis = _get_fallback_linear_axis(ax_type, doub_coords) else: - jaxis = _get_fallback_linear_axis(ax_type, sj.to_java(doub_coords)) + jaxis = _get_fallback_linear_axis(ax_type, doub_coords) else: - jaxis = _get_fallback_linear_axis(ax_type, sj.to_java(doub_coords)) + jaxis = _get_fallback_linear_axis(ax_type, doub_coords) axes[ax_num] = jaxis From 8ed5fa47dff2b87c9672845c0079c61644e85ee8 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 10 Feb 2023 07:40:02 -0600 Subject: [PATCH 10/41] Fix typo in jimport of VariableAxis --- src/imagej/_java.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imagej/_java.py b/src/imagej/_java.py index dd64798d..55b1bdfe 100644 --- a/src/imagej/_java.py +++ b/src/imagej/_java.py @@ -108,7 +108,7 @@ def RodbardAxis(self): @JavaClasses.java_import def VariableAxis(self): - return "net.iamgej.axis.VariableAxis" + return "net.imagej.axis.VariableAxis" @JavaClasses.java_import def Dataset(self): From d58d255be1b9cabe9f9c5bf4b4941806a6b90d62 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 10 Feb 2023 07:44:56 -0600 Subject: [PATCH 11/41] Use jc.CalibratedAxis for type hint for axes seqs --- src/imagej/convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/imagej/convert.py b/src/imagej/convert.py index 1a73001f..eee30511 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -4,7 +4,7 @@ import ctypes import logging import os -from typing import Dict, Sequence, Union +from typing import Dict, Sequence import imglyb import numpy as np @@ -564,7 +564,7 @@ def _rename_xarray_dims(xarr, new_dims: Sequence[str]): def _create_imagej_metadata( - axes: Sequence[Union["jc.DefaultLinearAxis", "jc.EnumeratedAxis"]], + axes: Sequence["jc.CalibratedAxis"], dim_seq: Sequence[str], ) -> dict: """ From fac7f33d17004e1714948a040f09331b576ebeaa Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 28 Mar 2023 10:04:50 -0500 Subject: [PATCH 12/41] Use if clause with ValueError instead of assert --- src/imagej/convert.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/imagej/convert.py b/src/imagej/convert.py index eee30511..7a744c72 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -571,7 +571,12 @@ def _create_imagej_metadata( Create the ImageJ metadata attribute dictionary for xarray's global attributes. """ ij_metadata = {} - assert len(axes) == len(dim_seq) + if len(axes) != len(dim_seq): + raise ValueError( + f"Axes length ({len(axes)}) does not match \ + dimension length ({len(dim_seq)})." + ) + for i in range(len(axes)): # get CalibratedAxis type as string (e.g. "EnumeratedAxis") ij_metadata[ From 0f175ddceb33cf57525c9cd62d31d2dca27aead4 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Sun, 2 Apr 2023 09:31:59 -0500 Subject: [PATCH 13/41] Add metadata submodule This commit adds a new metadata submodule to handle all image related metadata functions (e.g. creating/updating the xarray.DataArray metadata attribute). All metadata related functions should exist in this submodule. --- src/imagej/convert.py | 32 +---------- src/imagej/dims.py | 62 +++------------------- src/imagej/metadata.py | 118 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 85 deletions(-) create mode 100644 src/imagej/metadata.py diff --git a/src/imagej/convert.py b/src/imagej/convert.py index 7a744c72..a5a37a06 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -15,6 +15,7 @@ import imagej.dims as dims import imagej.images as images +import imagej.metadata as metadata from imagej._java import jc from imagej._java import log_exception as _log_exception @@ -232,7 +233,7 @@ def java_to_xarray(ij: "jc.ImageJ", jobj) -> xr.DataArray: xr_dims = list(permuted_rai.dims) xr_attrs = sj.to_python(permuted_rai.getProperties()) xr_attrs = {sj.to_python(k): sj.to_python(v) for k, v in xr_attrs.items()} - xr_attrs["imagej"] = _create_imagej_metadata(xr_axes, xr_dims) + xr_attrs["imagej"] = metadata.ImageMetadata.create_imagej_metadata(xr_axes, xr_dims) # reverse axes and dims to match narr xr_axes.reverse() xr_dims.reverse() @@ -563,35 +564,6 @@ def _rename_xarray_dims(xarr, new_dims: Sequence[str]): return xarr.rename(dim_map) -def _create_imagej_metadata( - axes: Sequence["jc.CalibratedAxis"], - dim_seq: Sequence[str], -) -> dict: - """ - Create the ImageJ metadata attribute dictionary for xarray's global attributes. - """ - ij_metadata = {} - if len(axes) != len(dim_seq): - raise ValueError( - f"Axes length ({len(axes)}) does not match \ - dimension length ({len(dim_seq)})." - ) - - for i in range(len(axes)): - # get CalibratedAxis type as string (e.g. "EnumeratedAxis") - ij_metadata[ - dims._to_ijdim(dim_seq[i]) + "_cal_axis_type" - ] = dims._cal_axis_to_str(axes[i]) - # get scale and origin for DefaultLinearAxis - if isinstance(axes[i], jc.DefaultLinearAxis): - ij_metadata[dims._to_ijdim(dim_seq[i]) + "_scale"] = float(axes[i].scale()) - ij_metadata[dims._to_ijdim(dim_seq[i]) + "_origin"] = float( - axes[i].origin() - ) - - return ij_metadata - - def _delete_labeling_files(filepath): """ Removes any Labeling data left over at filepath diff --git a/src/imagej/dims.py b/src/imagej/dims.py index b989f3ee..e001f778 100644 --- a/src/imagej/dims.py +++ b/src/imagej/dims.py @@ -9,6 +9,7 @@ import xarray as xr from jpype import JException, JObject +import imagej.metadata as metadata from imagej._java import jc from imagej.images import is_arraylike as _is_arraylike from imagej.images import is_xarraylike as _is_xarraylike @@ -215,10 +216,14 @@ def _assign_axes( if cal_axis_type == "DefaultLinearAxis": origin = xarr.attrs["imagej"][ij_dim + "_origin"] scale = xarr.attrs["imagej"][ij_dim + "_scale"] - jaxis = _str_to_cal_axis(cal_axis_type)(ax_type, scale, origin) + jaxis = metadata.Axis._str_to_cal_axis(cal_axis_type)( + ax_type, scale, origin + ) else: try: - jaxis = _str_to_cal_axis(cal_axis_type)(ax_type, doub_coords) + jaxis = metadata.Axis._str_to_cal_axis(cal_axis_type)( + ax_type, doub_coords + ) except (JException, TypeError): jaxis = _get_fallback_linear_axis(ax_type, doub_coords) else: @@ -475,56 +480,3 @@ def _to_ijdim(key: str) -> str: return ijdims[key] else: return key - - -def _cal_axis_to_str(key) -> str: - """ - Convert a CalibratedAxis class (e.g. net.imagej.axis.DefaultLinearAxis) to - a string. - """ - cal_axis_to_str = { - jc.ChapmanRichardsAxis: "ChapmanRichardsAxis", - jc.DefaultLinearAxis: "DefaultLinearAxis", - jc.EnumeratedAxis: "EnumeratedAxis", - jc.ExponentialAxis: "ExponentialAxis", - jc.ExponentialRecoveryAxis: "ExponentialRecoveryAxis", - jc.GammaVariateAxis: "GammaVariateAxis", - jc.GaussianAxis: "GaussianAxis", - jc.IdentityAxis: "IdentityAxis", - jc.InverseRodbardAxis: "InverseRodbardAxis", - jc.LogLinearAxis: "LogLinearAxis", - jc.PolynomialAxis: "PolynomialAxis", - jc.PowerAxis: "PowerAxis", - jc.RodbardAxis: "RodbardAxis", - } - - if key.__class__ in cal_axis_to_str: - return cal_axis_to_str[key.__class__] - else: - return "unknown" - - -def _str_to_cal_axis(key: str): - """ - Convert a string (e.g. "DefaultLinearAxis") to a CalibratedAxis class. - """ - str_to_cal_axis = { - "ChapmanRichardsAxis": jc.ChapmanRichardsAxis, - "DefaultLinearAxis": jc.DefaultLinearAxis, - "EnumeratedAxis": jc.EnumeratedAxis, - "ExponentialAxis": jc.ExponentialAxis, - "ExponentialRecoveryAxis": jc.ExponentialRecoveryAxis, - "GammaVariateAxis": jc.GammaVariateAxis, - "GaussianAxis": jc.GaussianAxis, - "IdentityAxis": jc.IdentityAxis, - "InverseRodbardAxis": jc.InverseRodbardAxis, - "LogLinearAxis": jc.LogLinearAxis, - "PolynomialAxis": jc.PolynomialAxis, - "PowerAxis": jc.PowerAxis, - "RodbardAxis": jc.RodbardAxis, - } - - if key in str_to_cal_axis: - return str_to_cal_axis[key] - else: - return None diff --git a/src/imagej/metadata.py b/src/imagej/metadata.py new file mode 100644 index 00000000..952173bc --- /dev/null +++ b/src/imagej/metadata.py @@ -0,0 +1,118 @@ +""" +Utility function for creating, editing and modifying metadata. +""" +from typing import Sequence + +from _jpype import JClass + +import imagej.dims as dims +from imagej._java import jc + + +class Axis: + """ + Utility class used to generate ImageJ2 axis metadata information. + """ + + _calibrated_axis_types = {} + + @classmethod + def _cal_axis_to_str(cls, axis: "jc.CalibratedAxis") -> str: + """ + Convert a CalibratedAxis class to a String. + :param axis: CalibratedAxis type (e.g. net.imagej.axis.DefaultLinearAxis). + :return: String of CalibratedAxis typeb(e.g. "DefaultLinearAxis"). + """ + if not cls._calibrated_axis_types: + cls._calibrated_axis_types = cls._create_calibrated_axis_dict() + + if not isinstance(axis, JClass): + axis = axis.__class__ + + if axis in cls._calibrated_axis_types.keys(): + return cls._calibrated_axis_types[axis] + else: + return "unknown" + + @classmethod + def _str_to_cal_axis(cls, axis: str) -> "jc.CalibratedAxis": + """ + Convert a String to CalibratedAxis class. + :param axis: String of calibratedAxis type (e.g. "DefaultLinearAxis"). + :return: Java class of CalibratedAxis type + (e.g. net.imagej.axis.DefaultLinearAxis). + """ + if not cls._calibrated_axis_types: + cls._calibrated_axis_types = cls._create_calibrated_axis_dict() + + if not isinstance(axis, str): + raise TypeError(f"Axis is not type string: {type(axis)}.") + + for k, v in cls._calibrated_axis_types.items(): + if axis == v: + return k + + return None + + @classmethod + def _create_calibrated_axis_dict(cls): + """ + Create the CalibratedAxis dictionary on demand. + """ + axis_types = { + jc.ChapmanRichardsAxis: "ChapmanRichardsAxis", + jc.DefaultLinearAxis: "DefaultLinearAxis", + jc.EnumeratedAxis: "EnumeratedAxis", + jc.ExponentialAxis: "ExponentialAxis", + jc.ExponentialRecoveryAxis: "ExponentialRecoveryAxis", + jc.GammaVariateAxis: "GammaVariateAxis", + jc.GaussianAxis: "GaussianAxis", + jc.IdentityAxis: "IdentityAxis", + jc.InverseRodbardAxis: "InverseRodbardAxis", + jc.LogLinearAxis: "LogLinearAxis", + jc.PolynomialAxis: "PolynomialAxis", + jc.PowerAxis: "PowerAxis", + jc.RodbardAxis: "RodbardAxis", + } + + return axis_types + + +class ImageMetadata: + """ + Utility class used to create and update image metadata. + """ + + @classmethod + def create_imagej_metadata( + cls, axes: Sequence["jc.CalibratedAxis"], dim_seq: Sequence[str] + ) -> dict: + """ + Create the ImageJ metadata attribute dictionary for xarray's global attributes. + :param axes: A list or tuple of ImageJ2 axis objects + (e.g. net.imagej.axis.DefaultLinearAxis). + :param dim_seq: A list or tuple of the dimension order (e.g. ['X', 'Y', 'C']). + :return: Dict of image metadata. + """ + ij_metadata = {} + if len(axes) != len(dim_seq): + raise ValueError( + f"Axes length ({len(axes)}) does not match \ + dimension length ({len(dim_seq)})." + ) + + for i in range(len(axes)): + # get CalibratedAxis type as string (e.g. "EnumeratedAxis") + ij_metadata[ + dims._to_ijdim(dim_seq[i]) + "_cal_axis_type" + ] = Axis._cal_axis_to_str(axes[i]) + # get scale and origin for DefaultLinearAxis + if isinstance(axes[i], jc.DefaultLinearAxis): + ij_metadata[dims._to_ijdim(dim_seq[i]) + "_scale"] = float( + axes[i].scale() + ) + ij_metadata[dims._to_ijdim(dim_seq[i]) + "_origin"] = float( + axes[i].origin() + ) + + return ij_metadata From 61a814c70293cf78437b6d5dd222c6687c97a544 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 4 Apr 2023 05:02:41 -0500 Subject: [PATCH 14/41] Use one-liner to fetch dict key Gabe suggested using a nice one-liner to get the CalibratedAxis from the dict instead of the if/else statement. --- src/imagej/metadata.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/imagej/metadata.py b/src/imagej/metadata.py index 952173bc..d326cd83 100644 --- a/src/imagej/metadata.py +++ b/src/imagej/metadata.py @@ -29,10 +29,7 @@ def _cal_axis_to_str(cls, axis: "jc.CalibratedAxis") -> str: if not isinstance(axis, JClass): axis = axis.__class__ - if axis in cls._calibrated_axis_types.keys(): - return cls._calibrated_axis_types[axis] - else: - return "unknown" + return cls._calibrated_axis_types.get(axis, "unknown") @classmethod def _str_to_cal_axis(cls, axis: str) -> "jc.CalibratedAxis": From 56ca2724a7f1a88fa30648d76c43427827a8aa49 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 14 Apr 2023 12:21:47 -0500 Subject: [PATCH 15/41] Refactor metadata module to be more pythonic The axis submodule has also been streamlined to drop the CalibratedAxis dict and instead uses a list of Strings. This avoids Java import errors if a user imports the axis submodule before initializing ImageJ. --- src/imagej/convert.py | 2 +- src/imagej/dims.py | 4 +- src/imagej/metadata.py | 115 -------------------------------- src/imagej/metadata/__init__.py | 37 ++++++++++ src/imagej/metadata/axis.py | 47 +++++++++++++ 5 files changed, 87 insertions(+), 118 deletions(-) delete mode 100644 src/imagej/metadata.py create mode 100644 src/imagej/metadata/__init__.py create mode 100644 src/imagej/metadata/axis.py diff --git a/src/imagej/convert.py b/src/imagej/convert.py index a5a37a06..16d6d3e0 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -233,7 +233,7 @@ def java_to_xarray(ij: "jc.ImageJ", jobj) -> xr.DataArray: xr_dims = list(permuted_rai.dims) xr_attrs = sj.to_python(permuted_rai.getProperties()) xr_attrs = {sj.to_python(k): sj.to_python(v) for k, v in xr_attrs.items()} - xr_attrs["imagej"] = metadata.ImageMetadata.create_imagej_metadata(xr_axes, xr_dims) + xr_attrs["imagej"] = metadata.create_imagej_metadata(xr_axes, xr_dims) # reverse axes and dims to match narr xr_axes.reverse() xr_dims.reverse() diff --git a/src/imagej/dims.py b/src/imagej/dims.py index e001f778..4bbd0685 100644 --- a/src/imagej/dims.py +++ b/src/imagej/dims.py @@ -216,12 +216,12 @@ def _assign_axes( if cal_axis_type == "DefaultLinearAxis": origin = xarr.attrs["imagej"][ij_dim + "_origin"] scale = xarr.attrs["imagej"][ij_dim + "_scale"] - jaxis = metadata.Axis._str_to_cal_axis(cal_axis_type)( + jaxis = metadata.axis.str_to_calibrated_axis(cal_axis_type)( ax_type, scale, origin ) else: try: - jaxis = metadata.Axis._str_to_cal_axis(cal_axis_type)( + jaxis = metadata.axis.str_to_calibrated_axis(cal_axis_type)( ax_type, doub_coords ) except (JException, TypeError): diff --git a/src/imagej/metadata.py b/src/imagej/metadata.py deleted file mode 100644 index d326cd83..00000000 --- a/src/imagej/metadata.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -Utility function for creating, editing and modifying metadata. -""" -from typing import Sequence - -from _jpype import JClass - -import imagej.dims as dims -from imagej._java import jc - - -class Axis: - """ - Utility class used to generate ImageJ2 axis metadata information. - """ - - _calibrated_axis_types = {} - - @classmethod - def _cal_axis_to_str(cls, axis: "jc.CalibratedAxis") -> str: - """ - Convert a CalibratedAxis class to a String. - :param axis: CalibratedAxis type (e.g. net.imagej.axis.DefaultLinearAxis). - :return: String of CalibratedAxis typeb(e.g. "DefaultLinearAxis"). - """ - if not cls._calibrated_axis_types: - cls._calibrated_axis_types = cls._create_calibrated_axis_dict() - - if not isinstance(axis, JClass): - axis = axis.__class__ - - return cls._calibrated_axis_types.get(axis, "unknown") - - @classmethod - def _str_to_cal_axis(cls, axis: str) -> "jc.CalibratedAxis": - """ - Convert a String to CalibratedAxis class. - :param axis: String of calibratedAxis type (e.g. "DefaultLinearAxis"). - :return: Java class of CalibratedAxis type - (e.g. net.imagej.axis.DefaultLinearAxis). - """ - if not cls._calibrated_axis_types: - cls._calibrated_axis_types = cls._create_calibrated_axis_dict() - - if not isinstance(axis, str): - raise TypeError(f"Axis is not type string: {type(axis)}.") - - for k, v in cls._calibrated_axis_types.items(): - if axis == v: - return k - - return None - - @classmethod - def _create_calibrated_axis_dict(cls): - """ - Create the CalibratedAxis dictionary on demand. - """ - axis_types = { - jc.ChapmanRichardsAxis: "ChapmanRichardsAxis", - jc.DefaultLinearAxis: "DefaultLinearAxis", - jc.EnumeratedAxis: "EnumeratedAxis", - jc.ExponentialAxis: "ExponentialAxis", - jc.ExponentialRecoveryAxis: "ExponentialRecoveryAxis", - jc.GammaVariateAxis: "GammaVariateAxis", - jc.GaussianAxis: "GaussianAxis", - jc.IdentityAxis: "IdentityAxis", - jc.InverseRodbardAxis: "InverseRodbardAxis", - jc.LogLinearAxis: "LogLinearAxis", - jc.PolynomialAxis: "PolynomialAxis", - jc.PowerAxis: "PowerAxis", - jc.RodbardAxis: "RodbardAxis", - } - - return axis_types - - -class ImageMetadata: - """ - Utility class used to create and update image metadata. - """ - - @classmethod - def create_imagej_metadata( - cls, axes: Sequence["jc.CalibratedAxis"], dim_seq: Sequence[str] - ) -> dict: - """ - Create the ImageJ metadata attribute dictionary for xarray's global attributes. - :param axes: A list or tuple of ImageJ2 axis objects - (e.g. net.imagej.axis.DefaultLinearAxis). - :param dim_seq: A list or tuple of the dimension order (e.g. ['X', 'Y', 'C']). - :return: Dict of image metadata. - """ - ij_metadata = {} - if len(axes) != len(dim_seq): - raise ValueError( - f"Axes length ({len(axes)}) does not match \ - dimension length ({len(dim_seq)})." - ) - - for i in range(len(axes)): - # get CalibratedAxis type as string (e.g. "EnumeratedAxis") - ij_metadata[ - dims._to_ijdim(dim_seq[i]) + "_cal_axis_type" - ] = Axis._cal_axis_to_str(axes[i]) - # get scale and origin for DefaultLinearAxis - if isinstance(axes[i], jc.DefaultLinearAxis): - ij_metadata[dims._to_ijdim(dim_seq[i]) + "_scale"] = float( - axes[i].scale() - ) - ij_metadata[dims._to_ijdim(dim_seq[i]) + "_origin"] = float( - axes[i].origin() - ) - - return ij_metadata diff --git a/src/imagej/metadata/__init__.py b/src/imagej/metadata/__init__.py new file mode 100644 index 00000000..459e2493 --- /dev/null +++ b/src/imagej/metadata/__init__.py @@ -0,0 +1,37 @@ +from typing import Sequence + +import imagej.dims as dims +import imagej.metadata.axis as axis +from imagej._java import jc + + +def create_imagej_metadata( + axes: Sequence["jc.CalibratedAxis"], dim_seq: Sequence[str] +) -> dict: + """ + Create the ImageJ metadata attribute dictionary for xarray's global attributes. + :param axes: A list or tuple of ImageJ2 axis objects + (e.g. net.imagej.axis.DefaultLinearAxis). + :param dim_seq: A list or tuple of the dimension order (e.g. ['X', 'Y', 'C']). + :return: Dict of image metadata. + """ + ij_metadata = {} + if len(axes) != len(dim_seq): + raise ValueError( + f"Axes length ({len(axes)}) does not match \ + dimension length ({len(dim_seq)})." + ) + + for i in range(len(axes)): + # get CalibratedAxis type as string (e.g. "EnumeratedAxis") + ij_metadata[ + dims._to_ijdim(dim_seq[i]) + "_cal_axis_type" + ] = axis.calibrated_axis_to_str(axes[i]) + # get scale and origin for DefaultLinearAxis + if isinstance(axes[i], jc.DefaultLinearAxis): + ij_metadata[dims._to_ijdim(dim_seq[i]) + "_scale"] = float(axes[i].scale()) + ij_metadata[dims._to_ijdim(dim_seq[i]) + "_origin"] = float( + axes[i].origin() + ) + + return ij_metadata diff --git a/src/imagej/metadata/axis.py b/src/imagej/metadata/axis.py new file mode 100644 index 00000000..b4395c99 --- /dev/null +++ b/src/imagej/metadata/axis.py @@ -0,0 +1,47 @@ +from _jpype import JClass + +from imagej._java import jc + +_calibrated_axes = [ + "net.imagej.axis.ChapmanRichardsAxis", + "net.imagej.axis.DefaultLinearAxis", + "net.imagej.axis.EnumeratedAxis", + "net.imagej.axis.ExponentialAxis", + "net.imagej.axis.ExponentialRecoveryAxis", + "net.imagej.axis.GammaVariateAxis", + "net.imagej.axis.GaussianAxis", + "net.imagej.axis.IdentityAxis", + "net.imagej.axis.InverseRodbardAxis", + "net.imagej.axis.LogLinearAxis", + "net.imagej.axis.PolynomialAxis", + "net.imagej.axis.PowerAxis", + "net.imagej.axis.RodbardAxis", +] + + +def calibrated_axis_to_str(axis: "jc.CalibratedAxis") -> str: + """ + Convert a CalibratedAxis class to a String. + :param axis: CalibratedAxis type (e.g. net.imagej.axis.DefaultLinearAxis). + :return: String of CalibratedAxis typeb(e.g. "DefaultLinearAxis"). + """ + if not isinstance(axis, JClass): + axis = axis.__class__ + + return str(axis).split("'")[1] + + +def str_to_calibrated_axis(axis: str) -> "jc.CalibratedAxis": + """ + Convert a String to CalibratedAxis class. + :param axis: String of calibratedAxis type (e.g. "DefaultLinearAxis"). + :return: Java class of CalibratedAxis type + (e.g. net.imagej.axis.DefaultLinearAxis). + """ + if not isinstance(axis, str): + raise TypeError(f"Axis {type(axis)} is not a String.") + + if axis in _calibrated_axes: + return getattr(jc, axis.split(".")[3]) + else: + return None From 8af2bdcb07f12feb5937de5cb13fc4b7a5b6ff7e Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 17 Apr 2023 14:41:34 -0500 Subject: [PATCH 16/41] Refactor create imagej metadata function --- src/imagej/convert.py | 2 +- src/imagej/metadata/__init__.py | 53 ++++++++++++++++----------------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/imagej/convert.py b/src/imagej/convert.py index 16d6d3e0..7ca724c5 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -233,7 +233,7 @@ def java_to_xarray(ij: "jc.ImageJ", jobj) -> xr.DataArray: xr_dims = list(permuted_rai.dims) xr_attrs = sj.to_python(permuted_rai.getProperties()) xr_attrs = {sj.to_python(k): sj.to_python(v) for k, v in xr_attrs.items()} - xr_attrs["imagej"] = metadata.create_imagej_metadata(xr_axes, xr_dims) + xr_attrs["imagej"] = metadata.create_imagej_metadata(permuted_rai) # reverse axes and dims to match narr xr_axes.reverse() xr_dims.reverse() diff --git a/src/imagej/metadata/__init__.py b/src/imagej/metadata/__init__.py index 459e2493..d67c9f21 100644 --- a/src/imagej/metadata/__init__.py +++ b/src/imagej/metadata/__init__.py @@ -1,37 +1,34 @@ from typing import Sequence -import imagej.dims as dims import imagej.metadata.axis as axis from imagej._java import jc -def create_imagej_metadata( - axes: Sequence["jc.CalibratedAxis"], dim_seq: Sequence[str] -) -> dict: - """ - Create the ImageJ metadata attribute dictionary for xarray's global attributes. - :param axes: A list or tuple of ImageJ2 axis objects - (e.g. net.imagej.axis.DefaultLinearAxis). - :param dim_seq: A list or tuple of the dimension order (e.g. ['X', 'Y', 'C']). - :return: Dict of image metadata. - """ - ij_metadata = {} - if len(axes) != len(dim_seq): - raise ValueError( - f"Axes length ({len(axes)}) does not match \ - dimension length ({len(dim_seq)})." - ) +def _create_axis_metadata(img: "jc.ImgPlus") -> Sequence[dict]: + meta_arr = [] + axes = list(img.dim_axes)[::-1] + dims = list(img.dims)[::-1] + shape = list(img.shape)[::-1] + + for i in range(len(dims)): + ax = axes[i] + ax_meta = {} + # get per axis metadata + ax_meta["label"] = dims[i] + ax_meta["length"] = shape[i] + ax_meta["CalibratedAxis"] = axis.calibrated_axis_to_str(ax) + if hasattr(ax, "scale"): + ax_meta["scale"] = float(ax.scale()) + if hasattr(ax, "origin"): + ax_meta["origin"] = float(ax.origin()) + # store metadata + meta_arr.append(ax_meta) - for i in range(len(axes)): - # get CalibratedAxis type as string (e.g. "EnumeratedAxis") - ij_metadata[ - dims._to_ijdim(dim_seq[i]) + "_cal_axis_type" - ] = axis.calibrated_axis_to_str(axes[i]) - # get scale and origin for DefaultLinearAxis - if isinstance(axes[i], jc.DefaultLinearAxis): - ij_metadata[dims._to_ijdim(dim_seq[i]) + "_scale"] = float(axes[i].scale()) - ij_metadata[dims._to_ijdim(dim_seq[i]) + "_origin"] = float( - axes[i].origin() - ) + return meta_arr + +def create_imagej_metadata(img: "jc.ImgPlus"): + ij_metadata = {} + # imagej metadata sections + ij_metadata["axis"] = _create_axis_metadata(img) return ij_metadata From 6cd7eb8831f8098c4be903179dc637bef111d26a Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 25 Apr 2023 13:58:28 -0500 Subject: [PATCH 17/41] Refactor _assign_axes to use new metadata layout --- src/imagej/dims.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/imagej/dims.py b/src/imagej/dims.py index 4bbd0685..91bd53aa 100644 --- a/src/imagej/dims.py +++ b/src/imagej/dims.py @@ -188,7 +188,8 @@ def _assign_axes( :return: A list of ImageJ Axis with the specified origin and scale. """ axes = [""] * xarr.ndim - for dim in xarr.dims: + for i in range(xarr.ndim): + dim = xarr.dims[i] axis_str = _convert_dim(dim, "java") ax_type = jc.Axes.get(axis_str) ax_num = _get_axis_num(xarr, dim) @@ -209,19 +210,17 @@ def _assign_axes( # assign calibrated axis type -- checks xarray for imagej metadata jaxis = None if "imagej" in xarr.attrs.keys(): - ij_dim = _convert_dim(dim, "java") - if ij_dim + "_cal_axis_type" in xarr.attrs["imagej"].keys(): - cal_axis_type = xarr.attrs["imagej"][ij_dim + "_cal_axis_type"] - # get scale from metadata if axis type is DefaultLinearAxis - if cal_axis_type == "DefaultLinearAxis": - origin = xarr.attrs["imagej"][ij_dim + "_origin"] - scale = xarr.attrs["imagej"][ij_dim + "_scale"] - jaxis = metadata.axis.str_to_calibrated_axis(cal_axis_type)( - ax_type, scale, origin + if "axis" in xarr.attrs["imagej"].keys(): + ax = xarr.attrs["imagej"]["axis"][i] + cal_type = ax["CalibratedAxis"].split(".")[3] + # case logic for various CalibratedAxis + if cal_type == "DefaultLinearAxis": + jaxis = metadata.axis.str_to_calibrated_axis(ax["CalibratedAxis"])( + ax_type, ax["scale"], ax["origin"] ) else: try: - jaxis = metadata.axis.str_to_calibrated_axis(cal_axis_type)( + jaxis = metadata.axis.str_to_calibrated_axis(cal_type)( ax_type, doub_coords ) except (JException, TypeError): From 1956351719d279169b4fc95a2fbec854fda2c5c8 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 25 Apr 2023 14:01:17 -0500 Subject: [PATCH 18/41] Improve docstring for axis functions The docstring wasn't clear about what kind of string to send to and from the calibrated axis helper functions. --- src/imagej/metadata/axis.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/imagej/metadata/axis.py b/src/imagej/metadata/axis.py index b4395c99..461ee2e8 100644 --- a/src/imagej/metadata/axis.py +++ b/src/imagej/metadata/axis.py @@ -22,8 +22,9 @@ def calibrated_axis_to_str(axis: "jc.CalibratedAxis") -> str: """ Convert a CalibratedAxis class to a String. - :param axis: CalibratedAxis type (e.g. net.imagej.axis.DefaultLinearAxis). - :return: String of CalibratedAxis typeb(e.g. "DefaultLinearAxis"). + :param axis: Java class of CalibratedAxis type. + :return: String of CalibratedAxis type + (e.g. "net.imagej.axis.DefaultLinearAxis"). """ if not isinstance(axis, JClass): axis = axis.__class__ @@ -34,9 +35,9 @@ def calibrated_axis_to_str(axis: "jc.CalibratedAxis") -> str: def str_to_calibrated_axis(axis: str) -> "jc.CalibratedAxis": """ Convert a String to CalibratedAxis class. - :param axis: String of calibratedAxis type (e.g. "DefaultLinearAxis"). - :return: Java class of CalibratedAxis type - (e.g. net.imagej.axis.DefaultLinearAxis). + :param axis: String of calibratedAxis type + (e.g. "net.imagej.axis.DefaultLinearAxis"). + :return: Java class of CalibratedAxis type. """ if not isinstance(axis, str): raise TypeError(f"Axis {type(axis)} is not a String.") From 615ee25c37b198527ce72024a6f18c5d5c6ec923 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 25 Apr 2023 14:03:32 -0500 Subject: [PATCH 19/41] Add type hint to create_imagej_metadata function --- src/imagej/metadata/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/imagej/metadata/__init__.py b/src/imagej/metadata/__init__.py index d67c9f21..49f53e1b 100644 --- a/src/imagej/metadata/__init__.py +++ b/src/imagej/metadata/__init__.py @@ -27,8 +27,9 @@ def _create_axis_metadata(img: "jc.ImgPlus") -> Sequence[dict]: return meta_arr -def create_imagej_metadata(img: "jc.ImgPlus"): +def create_imagej_metadata(img: "jc.ImgPlus") -> dict: ij_metadata = {} # imagej metadata sections ij_metadata["axis"] = _create_axis_metadata(img) + return ij_metadata From 88f5040eeefc16e5a42baad16e0a6c76d2fe8d5c Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 09:35:08 -0500 Subject: [PATCH 20/41] Remove axis submodule and axis to str methods The axis data are stored in the scifio.metadata.image map under the "axes" key. Converting this to a string is unecessary. --- src/imagej/metadata/__init__.py | 21 --------------- src/imagej/metadata/axis.py | 48 --------------------------------- 2 files changed, 69 deletions(-) delete mode 100644 src/imagej/metadata/axis.py diff --git a/src/imagej/metadata/__init__.py b/src/imagej/metadata/__init__.py index 49f53e1b..10812155 100644 --- a/src/imagej/metadata/__init__.py +++ b/src/imagej/metadata/__init__.py @@ -1,30 +1,9 @@ from typing import Sequence - -import imagej.metadata.axis as axis from imagej._java import jc -def _create_axis_metadata(img: "jc.ImgPlus") -> Sequence[dict]: - meta_arr = [] - axes = list(img.dim_axes)[::-1] - dims = list(img.dims)[::-1] - shape = list(img.shape)[::-1] - for i in range(len(dims)): - ax = axes[i] - ax_meta = {} - # get per axis metadata - ax_meta["label"] = dims[i] - ax_meta["length"] = shape[i] - ax_meta["CalibratedAxis"] = axis.calibrated_axis_to_str(ax) - if hasattr(ax, "scale"): - ax_meta["scale"] = float(ax.scale()) - if hasattr(ax, "origin"): - ax_meta["origin"] = float(ax.origin()) - # store metadata - meta_arr.append(ax_meta) - return meta_arr def create_imagej_metadata(img: "jc.ImgPlus") -> dict: diff --git a/src/imagej/metadata/axis.py b/src/imagej/metadata/axis.py deleted file mode 100644 index 461ee2e8..00000000 --- a/src/imagej/metadata/axis.py +++ /dev/null @@ -1,48 +0,0 @@ -from _jpype import JClass - -from imagej._java import jc - -_calibrated_axes = [ - "net.imagej.axis.ChapmanRichardsAxis", - "net.imagej.axis.DefaultLinearAxis", - "net.imagej.axis.EnumeratedAxis", - "net.imagej.axis.ExponentialAxis", - "net.imagej.axis.ExponentialRecoveryAxis", - "net.imagej.axis.GammaVariateAxis", - "net.imagej.axis.GaussianAxis", - "net.imagej.axis.IdentityAxis", - "net.imagej.axis.InverseRodbardAxis", - "net.imagej.axis.LogLinearAxis", - "net.imagej.axis.PolynomialAxis", - "net.imagej.axis.PowerAxis", - "net.imagej.axis.RodbardAxis", -] - - -def calibrated_axis_to_str(axis: "jc.CalibratedAxis") -> str: - """ - Convert a CalibratedAxis class to a String. - :param axis: Java class of CalibratedAxis type. - :return: String of CalibratedAxis type - (e.g. "net.imagej.axis.DefaultLinearAxis"). - """ - if not isinstance(axis, JClass): - axis = axis.__class__ - - return str(axis).split("'")[1] - - -def str_to_calibrated_axis(axis: str) -> "jc.CalibratedAxis": - """ - Convert a String to CalibratedAxis class. - :param axis: String of calibratedAxis type - (e.g. "net.imagej.axis.DefaultLinearAxis"). - :return: Java class of CalibratedAxis type. - """ - if not isinstance(axis, str): - raise TypeError(f"Axis {type(axis)} is not a String.") - - if axis in _calibrated_axes: - return getattr(jc, axis.split(".")[3]) - else: - return None From bcfa00227130664e696f9a1f014f67deaaf4f71c Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 09:45:14 -0500 Subject: [PATCH 21/41] Refactor the xarray metadata creation funciton --- src/imagej/metadata/__init__.py | 40 ++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/imagej/metadata/__init__.py b/src/imagej/metadata/__init__.py index 10812155..4864e777 100644 --- a/src/imagej/metadata/__init__.py +++ b/src/imagej/metadata/__init__.py @@ -1,14 +1,44 @@ from typing import Sequence +import scyjava as sj from imagej._java import jc +def _imgplus_metadata_to_python_metadata(img_metadata, py_metadata: dict = None): + if py_metadata is None: + py_metadata = {} + for k, v in img_metadata.items(): + py_metadata[sj.to_python(k)] = sj.to_python(v) + return py_metadata -def create_imagej_metadata(img: "jc.ImgPlus") -> dict: - ij_metadata = {} - # imagej metadata sections - ij_metadata["axis"] = _create_axis_metadata(img) +def _python_metadata_to_imgplus_metadata(py_metadata: dict): + return sj.to_java(py_metadata['imagej']) + + +def create_xarray_metadata(img: "jc.ImgPlus") -> dict: + """ + Create the ImageJ xarray.DataArray metadata. + + :param img: Input net.imagej.ImgPlus. + :retutn: A Python dict representing the ImageJ metadata. + """ + # create empty dict for metadata + py_metadata = {} + + # try to get ImgPlus metadata + try: + img_metadata = img.getProperties() + except AttributeError: + img_metadata = None + + # convert metadata to python and add to dict + if img_metadata is not None: + py_metadata = _imgplus_metadata_to_python_metadata(img_metadata, py_metadata) + + # add additional metadata + py_metadata['RGB'] = is_rgb_merged(img) + + return py_metadata - return ij_metadata From ccee1b3dbe11167eb213a97c5932a3203ace77a8 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 09:48:52 -0500 Subject: [PATCH 22/41] Add is_rgb_merged method to check if image is RGB --- src/imagej/_java.py | 4 ++++ src/imagej/metadata/__init__.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/imagej/_java.py b/src/imagej/_java.py index 55b1bdfe..e7d8fb25 100644 --- a/src/imagej/_java.py +++ b/src/imagej/_java.py @@ -166,6 +166,10 @@ def ImgView(self): def ImgLabeling(self): return "net.imglib2.roi.labeling.ImgLabeling" + @JavaClasses.java_import + def IntegerType(self): + return "net.imglib2.type.numeric.IntegerType" + @JavaClasses.java_import def Named(self): return "org.scijava.Named" diff --git a/src/imagej/metadata/__init__.py b/src/imagej/metadata/__init__.py index 4864e777..c9a19c0a 100644 --- a/src/imagej/metadata/__init__.py +++ b/src/imagej/metadata/__init__.py @@ -17,6 +17,34 @@ def _python_metadata_to_imgplus_metadata(py_metadata: dict): return sj.to_java(py_metadata['imagej']) +def is_rgb_merged(img: "jc.ImgPlus") -> bool: + """ + Check if the ImgPlus is RGB merged. + + :param img: An input net.imagej.ImgPlus + :return: bool + """ + e = img.firstElement() + # check if signed + if e.getMinValue() < 0: + return False + # check if integer type + if not isinstance(e, jc.IntegerType): + return False + # check if bits per pixel is 8 + if e.getBitsPerPixel() != 8: + return False + # check for channel dimension (returns -1 if missing) + ch_index = img.dimensionIndex(jc.Axes.CHANNEL) + if ch_index < 0: + return False + # check if channel dimension is size 3 (RGB) + if img.dimension(ch_index) != 3: + return False + + return True + + def create_xarray_metadata(img: "jc.ImgPlus") -> dict: """ Create the ImageJ xarray.DataArray metadata. From 80ed95a3ef66843d5233e866f330dfdf498734cc Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 09:51:24 -0500 Subject: [PATCH 23/41] Use the metadata module for image metadata This commit uses the metadata module to create the metadata for both xarray and java images. Images can now be transferred back and forth between Java and Python without loosing the metadata. --- src/imagej/convert.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/imagej/convert.py b/src/imagej/convert.py index 7ca724c5..2b92c95b 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -231,9 +231,8 @@ def java_to_xarray(ij: "jc.ImageJ", jobj) -> xr.DataArray: assert hasattr(permuted_rai, "dim_axes") xr_axes = list(permuted_rai.dim_axes) xr_dims = list(permuted_rai.dims) - xr_attrs = sj.to_python(permuted_rai.getProperties()) - xr_attrs = {sj.to_python(k): sj.to_python(v) for k, v in xr_attrs.items()} - xr_attrs["imagej"] = metadata.create_imagej_metadata(permuted_rai) + xr_attrs = {} + xr_attrs["imagej"] = metadata.create_xarray_metadata(permuted_rai) # reverse axes and dims to match narr xr_axes.reverse() xr_dims.reverse() @@ -511,12 +510,12 @@ def metadata_wrapper_to_dict(ij: "jc.ImageJ", metadata_wrapper: "jc.MetadataWrap #################### -def _assign_dataset_metadata(dataset: "jc.Dataset", attrs): +def _assign_dataset_metadata(dataset: "jc.Dataset", attrs: dict): """ :param dataset: ImageJ2 Dataset :param attrs: Dictionary containing metadata """ - dataset.getProperties().putAll(sj.to_java(attrs)) + dataset.getProperties().putAll(metadata._python_metadata_to_imgplus_metadata(attrs)) def _permute_rai_to_python(rich_rai: "jc.RandomAccessibleInterval"): From 28ab749584e9e847661751b1d0496c69dc8fde5f Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 09:55:21 -0500 Subject: [PATCH 24/41] Apply Black formatting I missed this one :(. --- src/imagej/metadata/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/imagej/metadata/__init__.py b/src/imagej/metadata/__init__.py index c9a19c0a..52a05268 100644 --- a/src/imagej/metadata/__init__.py +++ b/src/imagej/metadata/__init__.py @@ -1,5 +1,5 @@ -from typing import Sequence import scyjava as sj + from imagej._java import jc @@ -14,7 +14,7 @@ def _imgplus_metadata_to_python_metadata(img_metadata, py_metadata: dict = None) def _python_metadata_to_imgplus_metadata(py_metadata: dict): - return sj.to_java(py_metadata['imagej']) + return sj.to_java(py_metadata["imagej"]) def is_rgb_merged(img: "jc.ImgPlus") -> bool: @@ -66,7 +66,6 @@ def create_xarray_metadata(img: "jc.ImgPlus") -> dict: py_metadata = _imgplus_metadata_to_python_metadata(img_metadata, py_metadata) # add additional metadata - py_metadata['RGB'] = is_rgb_merged(img) + py_metadata["RGB"] = is_rgb_merged(img) return py_metadata - From 73c5734ff9069dcbcc685d9f1221928f4027727f Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 11:39:49 -0500 Subject: [PATCH 25/41] Add array module for xarray accessors This module contains the xarray accessors that extend the xarray.DataArrays with additional methods. This module must be imported before the accessors are found. No other code is needed to attach these accessors. --- src/imagej/array.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/imagej/array.py diff --git a/src/imagej/array.py b/src/imagej/array.py new file mode 100644 index 00000000..c5fb9d29 --- /dev/null +++ b/src/imagej/array.py @@ -0,0 +1,22 @@ +import xarray as xr + + +@xr.register_dataarray_accessor("img") +class ImgAccessor: + def __init__(self, xarr): + self._data = xarr + + @property + def is_rgb(self): + return + + +@xr.register_dataarray_accessor("metadata") +class MetadataAccessor: + def __init__(self, xarr): + self._data = xarr + self._metadata = None + + @property + def axes(self): + return From b88e51330c89aeca28ec9296e2fab648e1b4a60b Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 11:51:18 -0500 Subject: [PATCH 26/41] Add set and get methods to MetadataAccessor --- src/imagej/array.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/imagej/array.py b/src/imagej/array.py index c5fb9d29..26743f53 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -20,3 +20,19 @@ def __init__(self, xarr): @property def axes(self): return + + def set(self, metadata: dict): + """ + Set the metadata of the parent xarray.DataArray. + + :param metadata: A Python dict representing the image metadata. + """ + self._metadata = metadata + + def get(self): + """ + Get the metadata dict of the the parent xarray.DataArray. + + :return: A Python dict representing the image metadata. + """ + return self._metadata From 205cff9734817b6e2724b9d97e129ecc9da86438 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 12:05:06 -0500 Subject: [PATCH 27/41] Add axes property to MetadataAccessor --- src/imagej/array.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/imagej/array.py b/src/imagej/array.py index 26743f53..a270b514 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -19,7 +19,16 @@ def __init__(self, xarr): @property def axes(self): - return + """ + Returns a tuple of the ImageJ axes. + + :return: A Python tuple of the ImageJ axes. + """ + return ( + tuple(self._metadata.get("scifio.metadata.image").get("axes")) + if "scifio.metadata.image" in self._metadata + else None + ) def set(self, metadata: dict): """ From 097c907a92807d09a45c7a3aa649c7efe67ddc7b Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 14:38:58 -0500 Subject: [PATCH 28/41] Add tree method to MetadataAccessor Once the metadata has been set call the tree() method to print a dict tree. --- src/imagej/array.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/imagej/array.py b/src/imagej/array.py index a270b514..b4ee3414 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -45,3 +45,22 @@ def get(self): :return: A Python dict representing the image metadata. """ return self._metadata + + def tree(self): + """ + Print a tree of the metadata of the parent xarray.DataArray. + """ + self._print_dict_tree(self._metadata) + + def _print_dict_tree(self, dictionary, indent="", prefix=""): + for idx, (key, value) in enumerate(dictionary.items()): + if idx == len(dictionary) - 1: + connector = "└──" + else: + connector = "├──" + print(indent + connector + prefix + " " + str(key)) + if isinstance(value, dict): + if idx == len(dictionary) - 1: + self._print_dict_tree(value, indent + " ", prefix="── ") + else: + self._print_dict_tree(value, indent + "│ ", prefix="── ") From 94db806caeff899b48ac462d05df495bee7012b8 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 14:56:43 -0500 Subject: [PATCH 29/41] Add type check for JavaMap to metadata tree func --- src/imagej/array.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/imagej/array.py b/src/imagej/array.py index b4ee3414..96b9076e 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -1,4 +1,5 @@ import xarray as xr +from scyjava import _convert @xr.register_dataarray_accessor("img") @@ -59,7 +60,7 @@ def _print_dict_tree(self, dictionary, indent="", prefix=""): else: connector = "├──" print(indent + connector + prefix + " " + str(key)) - if isinstance(value, dict): + if isinstance(value, (dict, _convert.JavaMap)): if idx == len(dictionary) - 1: self._print_dict_tree(value, indent + " ", prefix="── ") else: From e2812210e774b3629f496dc1ff935307c4436f79 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 15:01:28 -0500 Subject: [PATCH 30/41] Use xarray MetadataAccessor class for metadata --- src/imagej/convert.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/imagej/convert.py b/src/imagej/convert.py index 2b92c95b..5d10fe4b 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -13,9 +13,9 @@ from jpype import JByte, JException, JFloat, JLong, JObject, JShort from labeling import Labeling +import imagej.array # need to import to setup the accessor import imagej.dims as dims import imagej.images as images -import imagej.metadata as metadata from imagej._java import jc from imagej._java import log_exception as _log_exception @@ -167,7 +167,10 @@ def xarray_to_dataset(ij: "jc.ImageJ", xarr) -> "jc.Dataset": axes = dims._assign_axes(xarr) dataset.setAxes(axes) dataset.setName(xarr.name) - _assign_dataset_metadata(dataset, xarr.attrs) + if hasattr(xarr, "metadata"): + _assign_dataset_metadata(dataset, xarr.metadata.get()) + else: + _assign_dataset_metadata(dataset, xarr.attrs) return dataset @@ -231,15 +234,16 @@ def java_to_xarray(ij: "jc.ImageJ", jobj) -> xr.DataArray: assert hasattr(permuted_rai, "dim_axes") xr_axes = list(permuted_rai.dim_axes) xr_dims = list(permuted_rai.dims) - xr_attrs = {} - xr_attrs["imagej"] = metadata.create_xarray_metadata(permuted_rai) # reverse axes and dims to match narr xr_axes.reverse() xr_dims.reverse() xr_dims = dims._convert_dims(xr_dims, direction="python") xr_coords = dims._get_axes_coords(xr_axes, xr_dims, narr.shape) name = jobj.getName() if isinstance(jobj, jc.Named) else None - return xr.DataArray(narr, dims=xr_dims, coords=xr_coords, attrs=xr_attrs, name=name) + # use the MetadataAccessor to add metadata to the xarray + xarr = xr.DataArray(narr, dims=xr_dims, coords=xr_coords, name=name) + xarr.metadata.set(dict(sj.to_python(permuted_rai.getProperties()))) + return xarr def supports_java_to_ndarray(ij: "jc.ImageJ", obj) -> bool: @@ -515,7 +519,7 @@ def _assign_dataset_metadata(dataset: "jc.Dataset", attrs: dict): :param dataset: ImageJ2 Dataset :param attrs: Dictionary containing metadata """ - dataset.getProperties().putAll(metadata._python_metadata_to_imgplus_metadata(attrs)) + dataset.getProperties().putAll(sj.to_java(attrs)) def _permute_rai_to_python(rich_rai: "jc.RandomAccessibleInterval"): From f2aef68ff57983df79d72f7a4c315ff59e9b9bb8 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 18 May 2023 15:08:35 -0500 Subject: [PATCH 31/41] Add Flake8 bypass for imagej.array import --- src/imagej/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imagej/convert.py b/src/imagej/convert.py index 5d10fe4b..0539342c 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -13,7 +13,7 @@ from jpype import JByte, JException, JFloat, JLong, JObject, JShort from labeling import Labeling -import imagej.array # need to import to setup the accessor +import imagej.array # noqa:F401 import imagej.dims as dims import imagej.images as images from imagej._java import jc From 4c5bba6e3dbbdfdaf4cdf0c3284b6a9cda20b251 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 19 May 2023 11:38:49 -0500 Subject: [PATCH 32/41] Store the metadata dict in the xarray global attr This preserves the metadata dict between slices. --- src/imagej/array.py | 11 +++++------ src/imagej/convert.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/imagej/array.py b/src/imagej/array.py index 96b9076e..f1656c35 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -16,7 +16,6 @@ def is_rgb(self): class MetadataAccessor: def __init__(self, xarr): self._data = xarr - self._metadata = None @property def axes(self): @@ -26,8 +25,8 @@ def axes(self): :return: A Python tuple of the ImageJ axes. """ return ( - tuple(self._metadata.get("scifio.metadata.image").get("axes")) - if "scifio.metadata.image" in self._metadata + tuple(self._data.attrs["imagej"].get("scifio.metadata.image").get("axes")) + if "scifio.metadata.image" in self._data.attrs["imagej"] else None ) @@ -37,7 +36,7 @@ def set(self, metadata: dict): :param metadata: A Python dict representing the image metadata. """ - self._metadata = metadata + self._data.attrs["imagej"] = metadata def get(self): """ @@ -45,13 +44,13 @@ def get(self): :return: A Python dict representing the image metadata. """ - return self._metadata + return self._data.attrs["imagej"] def tree(self): """ Print a tree of the metadata of the parent xarray.DataArray. """ - self._print_dict_tree(self._metadata) + self._print_dict_tree(self._data.attrs["imagej"]) def _print_dict_tree(self, dictionary, indent="", prefix=""): for idx, (key, value) in enumerate(dictionary.items()): diff --git a/src/imagej/convert.py b/src/imagej/convert.py index 0539342c..9ebb0b71 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -170,7 +170,7 @@ def xarray_to_dataset(ij: "jc.ImageJ", xarr) -> "jc.Dataset": if hasattr(xarr, "metadata"): _assign_dataset_metadata(dataset, xarr.metadata.get()) else: - _assign_dataset_metadata(dataset, xarr.attrs) + _assign_dataset_metadata(dataset, xarr.attrs['imagej']) return dataset From 93d6ba96a4d6b3d5e2a7d3e9ae03bf21ed68fa56 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 22 May 2023 11:06:36 -0500 Subject: [PATCH 33/41] Add _update method to MetadataAccessor The _update() method runs any time the MetadataAccerssor is accessed. This allows us to update the metadata base by checking the state of the backing xarray.DataArray. So for example if dimensions change order or are dropped the "scifio.metadata.image" (if present) metadata should reflect these changes where appropriate. --- src/imagej/array.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/imagej/array.py b/src/imagej/array.py index f1656c35..4735a0de 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -1,6 +1,8 @@ import xarray as xr from scyjava import _convert +import imagej.dims as dims + @xr.register_dataarray_accessor("img") class ImgAccessor: @@ -16,6 +18,7 @@ def is_rgb(self): class MetadataAccessor: def __init__(self, xarr): self._data = xarr + self._update() @property def axes(self): @@ -64,3 +67,12 @@ def _print_dict_tree(self, dictionary, indent="", prefix=""): self._print_dict_tree(value, indent + " ", prefix="── ") else: self._print_dict_tree(value, indent + "│ ", prefix="── ") + + def _update(self): + if self._data.attrs.get("imagej"): + axes = [] + for i in range(len(self.axes)): + axis_label = dims._convert_dim(self.axes[i].type().getLabel(), "python") + if axis_label in self._data.dims: + axes.append(self.axes[i]) + self._data.attrs["imagej"].get("scifio.metadata.image", {})["axes"] = axes From 30c4fa36010f903d2fcba707ff19d8c35c37af69 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 22 May 2023 11:08:55 -0500 Subject: [PATCH 34/41] Pre-create the "image" xarr attr dict --- src/imagej/convert.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/imagej/convert.py b/src/imagej/convert.py index 9ebb0b71..8bf9eb82 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -170,7 +170,7 @@ def xarray_to_dataset(ij: "jc.ImageJ", xarr) -> "jc.Dataset": if hasattr(xarr, "metadata"): _assign_dataset_metadata(dataset, xarr.metadata.get()) else: - _assign_dataset_metadata(dataset, xarr.attrs['imagej']) + _assign_dataset_metadata(dataset, xarr.attrs["imagej"]) return dataset @@ -240,8 +240,9 @@ def java_to_xarray(ij: "jc.ImageJ", jobj) -> xr.DataArray: xr_dims = dims._convert_dims(xr_dims, direction="python") xr_coords = dims._get_axes_coords(xr_axes, xr_dims, narr.shape) name = jobj.getName() if isinstance(jobj, jc.Named) else None + xr_attrs = {"imagej": {}} + xarr = xr.DataArray(narr, dims=xr_dims, coords=xr_coords, name=name, attrs=xr_attrs) # use the MetadataAccessor to add metadata to the xarray - xarr = xr.DataArray(narr, dims=xr_dims, coords=xr_coords, name=name) xarr.metadata.set(dict(sj.to_python(permuted_rai.getProperties()))) return xarr From b8f3b6cd499961c418dba029eb50b5cd806eec00 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 22 May 2023 11:53:37 -0500 Subject: [PATCH 35/41] Add "axisLength" metadata update to array module --- src/imagej/array.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/imagej/array.py b/src/imagej/array.py index 4735a0de..fb472137 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -70,9 +70,28 @@ def _print_dict_tree(self, dictionary, indent="", prefix=""): def _update(self): if self._data.attrs.get("imagej"): + # update axes axes = [] for i in range(len(self.axes)): - axis_label = dims._convert_dim(self.axes[i].type().getLabel(), "python") - if axis_label in self._data.dims: + ax_label = dims._convert_dim(self.axes[i].type().getLabel(), "python") + if ax_label in self._data.dims: axes.append(self.axes[i]) self._data.attrs["imagej"].get("scifio.metadata.image", {})["axes"] = axes + + # update axis lengths + old_ax_len_metadata = ( + self._data.attrs["imagej"] + .get("scifio.metadata.image", {}) + .get("axisLengths", {}) + ) + new_ax_len_metadata = {} + for i in range(len(self.axes)): + ax_type = self.axes[i].type() + if ax_type in old_ax_len_metadata.keys(): + # update axis length + ax_label = dims._convert_dim(ax_type.getLabel(), "python") + curr_ax_len = self._data.shape[self._data.dims.index(ax_label)] + new_ax_len_metadata[ax_type] = curr_ax_len + self._data.attrs["imagej"].get("scifio.metadata.image", {})[ + "axisLengths" + ] = new_ax_len_metadata From 58f19b1710d41942be565107c2e0ec2253670c1d Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 22 May 2023 12:31:30 -0500 Subject: [PATCH 36/41] Add logic for is_rgb method to ImgAccessor --- src/imagej/array.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/imagej/array.py b/src/imagej/array.py index fb472137..a6105bec 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -1,3 +1,4 @@ +import numpy as np import xarray as xr from scyjava import _convert @@ -11,7 +12,32 @@ def __init__(self, xarr): @property def is_rgb(self): - return + """ + Returns True or False if the xarray.DataArray is an RGB image. + + :return: Boolean + """ + ch_labels = ["c", "ch", "Channel"] + # check if array is signed + if self._data.min() < 0: + return False + # check if array is integer dtype + if not np.issubdtype(self._data.data.dtype, np.integer): + return False + # check bitsperpixel + if self._data.dtype.itemsize * 8 != 8: + return False + # check if "channel" present + if not any(dim in self._data.dims for dim in ch_labels): + return False + # check channel length = 3 exactly + for dim in self._data.dims: + if dim in ch_labels: + loc = self._data.dims.index(dim) + if self._data.shape[loc] != 3: + return False + + return True @xr.register_dataarray_accessor("metadata") From 3beccd9ad0609405a80a37da00d8e29546c52233 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 22 May 2023 14:14:36 -0500 Subject: [PATCH 37/41] Fix metadata axes sorting The axes were still in ImageJ order. They now match and check against the dimension order of the parent xarray.DataArray. Note that the metadata must be updated manually after creation to update the order from Java to Python. --- src/imagej/array.py | 5 ++--- src/imagej/convert.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/imagej/array.py b/src/imagej/array.py index a6105bec..0868eca1 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -97,11 +97,10 @@ def _print_dict_tree(self, dictionary, indent="", prefix=""): def _update(self): if self._data.attrs.get("imagej"): # update axes - axes = [] + axes = [None] * len(self._data.dims) for i in range(len(self.axes)): ax_label = dims._convert_dim(self.axes[i].type().getLabel(), "python") - if ax_label in self._data.dims: - axes.append(self.axes[i]) + axes[self._data.dims.index(ax_label)] = self.axes[i] self._data.attrs["imagej"].get("scifio.metadata.image", {})["axes"] = axes # update axis lengths diff --git a/src/imagej/convert.py b/src/imagej/convert.py index 8bf9eb82..1ca46984 100644 --- a/src/imagej/convert.py +++ b/src/imagej/convert.py @@ -244,6 +244,7 @@ def java_to_xarray(ij: "jc.ImageJ", jobj) -> xr.DataArray: xarr = xr.DataArray(narr, dims=xr_dims, coords=xr_coords, name=name, attrs=xr_attrs) # use the MetadataAccessor to add metadata to the xarray xarr.metadata.set(dict(sj.to_python(permuted_rai.getProperties()))) + xarr.metadata._update() return xarr From d0b208b0c369b156a1be0c3088cdf2e257ea827d Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 22 May 2023 14:16:45 -0500 Subject: [PATCH 38/41] Remove duplicate comment --- src/imagej/array.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/imagej/array.py b/src/imagej/array.py index 0868eca1..1a5ce946 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -113,7 +113,6 @@ def _update(self): for i in range(len(self.axes)): ax_type = self.axes[i].type() if ax_type in old_ax_len_metadata.keys(): - # update axis length ax_label = dims._convert_dim(ax_type.getLabel(), "python") curr_ax_len = self._data.shape[self._data.dims.index(ax_label)] new_ax_len_metadata[ax_type] = curr_ax_len From 12da19ef5bb117b10a3d78029dae456bde0c1258 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 22 May 2023 15:02:52 -0500 Subject: [PATCH 39/41] Add check for axis labels in xarray dims --- src/imagej/array.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/imagej/array.py b/src/imagej/array.py index 1a5ce946..7c939e96 100644 --- a/src/imagej/array.py +++ b/src/imagej/array.py @@ -100,7 +100,8 @@ def _update(self): axes = [None] * len(self._data.dims) for i in range(len(self.axes)): ax_label = dims._convert_dim(self.axes[i].type().getLabel(), "python") - axes[self._data.dims.index(ax_label)] = self.axes[i] + if ax_label in self._data.dims: + axes[self._data.dims.index(ax_label)] = self.axes[i] self._data.attrs["imagej"].get("scifio.metadata.image", {})["axes"] = axes # update axis lengths From 5b611c40deda5740674b2d607c61072a9f6019b1 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 22 May 2023 15:04:19 -0500 Subject: [PATCH 40/41] Use MetadataAccessor to assign dataset axes Use the MetadataAccessor class of the xarray to assign the correct axes. Fallback to a DefaultLinearAxis if the metadata is not available. --- src/imagej/dims.py | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/src/imagej/dims.py b/src/imagej/dims.py index 91bd53aa..eab37e33 100644 --- a/src/imagej/dims.py +++ b/src/imagej/dims.py @@ -7,9 +7,8 @@ import numpy as np import scyjava as sj import xarray as xr -from jpype import JException, JObject +from jpype import JObject -import imagej.metadata as metadata from imagej._java import jc from imagej.images import is_arraylike as _is_arraylike from imagej.images import is_xarraylike as _is_xarraylike @@ -207,30 +206,11 @@ def _assign_axes( jc.Double(np.double(x)) for x in np.arrange(len(xarr.coords[dim])) ] - # assign calibrated axis type -- checks xarray for imagej metadata - jaxis = None - if "imagej" in xarr.attrs.keys(): - if "axis" in xarr.attrs["imagej"].keys(): - ax = xarr.attrs["imagej"]["axis"][i] - cal_type = ax["CalibratedAxis"].split(".")[3] - # case logic for various CalibratedAxis - if cal_type == "DefaultLinearAxis": - jaxis = metadata.axis.str_to_calibrated_axis(ax["CalibratedAxis"])( - ax_type, ax["scale"], ax["origin"] - ) - else: - try: - jaxis = metadata.axis.str_to_calibrated_axis(cal_type)( - ax_type, doub_coords - ) - except (JException, TypeError): - jaxis = _get_fallback_linear_axis(ax_type, doub_coords) - else: - jaxis = _get_fallback_linear_axis(ax_type, doub_coords) + # use the xarr metadata if available to assign axes + if hasattr(xarr, "metadata") and xarr.metadata.axes: + axes[ax_num] = xarr.metadata.axes[i] else: - jaxis = _get_fallback_linear_axis(ax_type, doub_coords) - - axes[ax_num] = jaxis + axes[ax_num] = _get_fallback_linear_axis(ax_type, doub_coords) return axes From 0b1ab8b22daf56c788f9037bf226e2d79de1464d Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 22 May 2023 15:56:53 -0500 Subject: [PATCH 41/41] Remove metadata module This is no longer in needed. --- src/imagej/metadata/__init__.py | 71 --------------------------------- 1 file changed, 71 deletions(-) delete mode 100644 src/imagej/metadata/__init__.py diff --git a/src/imagej/metadata/__init__.py b/src/imagej/metadata/__init__.py deleted file mode 100644 index 52a05268..00000000 --- a/src/imagej/metadata/__init__.py +++ /dev/null @@ -1,71 +0,0 @@ -import scyjava as sj - -from imagej._java import jc - - -def _imgplus_metadata_to_python_metadata(img_metadata, py_metadata: dict = None): - if py_metadata is None: - py_metadata = {} - - for k, v in img_metadata.items(): - py_metadata[sj.to_python(k)] = sj.to_python(v) - - return py_metadata - - -def _python_metadata_to_imgplus_metadata(py_metadata: dict): - return sj.to_java(py_metadata["imagej"]) - - -def is_rgb_merged(img: "jc.ImgPlus") -> bool: - """ - Check if the ImgPlus is RGB merged. - - :param img: An input net.imagej.ImgPlus - :return: bool - """ - e = img.firstElement() - # check if signed - if e.getMinValue() < 0: - return False - # check if integer type - if not isinstance(e, jc.IntegerType): - return False - # check if bits per pixel is 8 - if e.getBitsPerPixel() != 8: - return False - # check for channel dimension (returns -1 if missing) - ch_index = img.dimensionIndex(jc.Axes.CHANNEL) - if ch_index < 0: - return False - # check if channel dimension is size 3 (RGB) - if img.dimension(ch_index) != 3: - return False - - return True - - -def create_xarray_metadata(img: "jc.ImgPlus") -> dict: - """ - Create the ImageJ xarray.DataArray metadata. - - :param img: Input net.imagej.ImgPlus. - :retutn: A Python dict representing the ImageJ metadata. - """ - # create empty dict for metadata - py_metadata = {} - - # try to get ImgPlus metadata - try: - img_metadata = img.getProperties() - except AttributeError: - img_metadata = None - - # convert metadata to python and add to dict - if img_metadata is not None: - py_metadata = _imgplus_metadata_to_python_metadata(img_metadata, py_metadata) - - # add additional metadata - py_metadata["RGB"] = is_rgb_merged(img) - - return py_metadata