diff --git a/tools/2d_feature_extraction/2d_feature_extraction.py b/tools/2d_feature_extraction/2d_feature_extraction.py
index 3b7584dda..90e2e81cf 100644
--- a/tools/2d_feature_extraction/2d_feature_extraction.py
+++ b/tools/2d_feature_extraction/2d_feature_extraction.py
@@ -1,127 +1,115 @@
-import argparse
-
-import giatools.io
+import giatools
import numpy as np
import pandas as pd
-import skimage.feature
+import scipy.ndimage as ndi
import skimage.measure
-import skimage.morphology
-import skimage.segmentation
+# Fail early if an optional backend is not available
+giatools.require_backend('omezarr')
-if __name__ == '__main__':
- parser = argparse.ArgumentParser(description='Extract image features')
-
- # TODO create factory for boilerplate code
- features = parser.add_argument_group('compute features')
- features.add_argument('--all', dest='all_features', action='store_true')
- features.add_argument('--label', dest='add_label', action='store_true')
- features.add_argument('--patches', dest='add_roi_patches', action='store_true')
- features.add_argument('--max_intensity', dest='max_intensity', action='store_true')
- features.add_argument('--mean_intensity', dest='mean_intensity', action='store_true')
- features.add_argument('--min_intensity', dest='min_intensity', action='store_true')
- features.add_argument('--moments_hu', dest='moments_hu', action='store_true')
- features.add_argument('--centroid', dest='centroid', action='store_true')
- features.add_argument('--bbox', dest='bbox', action='store_true')
- features.add_argument('--area', dest='area', action='store_true')
- features.add_argument('--filled_area', dest='filled_area', action='store_true')
- features.add_argument('--convex_area', dest='convex_area', action='store_true')
- features.add_argument('--perimeter', dest='perimeter', action='store_true')
- features.add_argument('--extent', dest='extent', action='store_true')
- features.add_argument('--eccentricity', dest='eccentricity', action='store_true')
- features.add_argument('--equivalent_diameter', dest='equivalent_diameter', action='store_true')
- features.add_argument('--euler_number', dest='euler_number', action='store_true')
- features.add_argument('--inertia_tensor_eigvals', dest='inertia_tensor_eigvals', action='store_true')
- features.add_argument('--major_axis_length', dest='major_axis_length', action='store_true')
- features.add_argument('--minor_axis_length', dest='minor_axis_length', action='store_true')
- features.add_argument('--orientation', dest='orientation', action='store_true')
- features.add_argument('--solidity', dest='solidity', action='store_true')
- features.add_argument('--moments', dest='moments', action='store_true')
- features.add_argument('--convexity', dest='convexity', action='store_true')
-
- parser.add_argument('--label_file_binary', dest='label_file_binary', action='store_true')
-
- parser.add_argument('--raw', dest='raw_file', type=argparse.FileType('r'),
- help='Original input file', required=False)
- parser.add_argument('label_file', type=argparse.FileType('r'),
- help='Label input file')
- parser.add_argument('output_file', type=argparse.FileType('w'),
- help='Tabular output file')
- args = parser.parse_args()
-
- label_file_binary = args.label_file_binary
- label_file = args.label_file.name
- out_file = args.output_file.name
- add_patch = args.add_roi_patches
-
- raw_image = None
- if args.raw_file is not None:
- raw_image = giatools.io.imread(args.raw_file.name)
-
- raw_label_image = giatools.io.imread(label_file)
-
- df = pd.DataFrame()
- if label_file_binary:
- raw_label_image = skimage.measure.label(raw_label_image)
- regions = skimage.measure.regionprops(raw_label_image, intensity_image=raw_image)
-
- df['it'] = np.arange(len(regions))
-
- if add_patch:
- df['image'] = df['it'].map(lambda ait: regions[ait].image.astype(np.float).tolist())
- df['intensity_image'] = df['it'].map(lambda ait: regions[ait].intensity_image.astype(np.float).tolist())
-
- # TODO no matrix features, but split in own rows?
- if args.add_label or args.all_features:
- df['label'] = df['it'].map(lambda ait: regions[ait].label)
-
- if raw_image is not None:
- if args.max_intensity or args.all_features:
- df['max_intensity'] = df['it'].map(lambda ait: regions[ait].max_intensity)
- if args.mean_intensity or args.all_features:
- df['mean_intensity'] = df['it'].map(lambda ait: regions[ait].mean_intensity)
- if args.min_intensity or args.all_features:
- df['min_intensity'] = df['it'].map(lambda ait: regions[ait].min_intensity)
- if args.moments_hu or args.all_features:
- df['moments_hu'] = df['it'].map(lambda ait: regions[ait].moments_hu)
-
- if args.centroid or args.all_features:
- df['centroid'] = df['it'].map(lambda ait: regions[ait].centroid)
- if args.bbox or args.all_features:
- df['bbox'] = df['it'].map(lambda ait: regions[ait].bbox)
- if args.area or args.all_features:
- df['area'] = df['it'].map(lambda ait: regions[ait].area)
- if args.filled_area or args.all_features:
- df['filled_area'] = df['it'].map(lambda ait: regions[ait].filled_area)
- if args.convex_area or args.all_features:
- df['convex_area'] = df['it'].map(lambda ait: regions[ait].convex_area)
- if args.perimeter or args.all_features:
- df['perimeter'] = df['it'].map(lambda ait: regions[ait].perimeter)
- if args.extent or args.all_features:
- df['extent'] = df['it'].map(lambda ait: regions[ait].extent)
- if args.eccentricity or args.all_features:
- df['eccentricity'] = df['it'].map(lambda ait: regions[ait].eccentricity)
- if args.equivalent_diameter or args.all_features:
- df['equivalent_diameter'] = df['it'].map(lambda ait: regions[ait].equivalent_diameter)
- if args.euler_number or args.all_features:
- df['euler_number'] = df['it'].map(lambda ait: regions[ait].euler_number)
- if args.inertia_tensor_eigvals or args.all_features:
- df['inertia_tensor_eigvals'] = df['it'].map(lambda ait: regions[ait].inertia_tensor_eigvals)
- if args.major_axis_length or args.all_features:
- df['major_axis_length'] = df['it'].map(lambda ait: regions[ait].major_axis_length)
- if args.minor_axis_length or args.all_features:
- df['minor_axis_length'] = df['it'].map(lambda ait: regions[ait].minor_axis_length)
- if args.orientation or args.all_features:
- df['orientation'] = df['it'].map(lambda ait: regions[ait].orientation)
- if args.solidity or args.all_features:
- df['solidity'] = df['it'].map(lambda ait: regions[ait].solidity)
- if args.moments or args.all_features:
- df['moments'] = df['it'].map(lambda ait: regions[ait].moments)
- if args.convexity or args.all_features:
- perimeter = df['it'].map(lambda ait: regions[ait].perimeter)
- area = df['it'].map(lambda ait: regions[ait].area)
- df['convexity'] = area / (perimeter * perimeter)
-
- del df['it']
- df.to_csv(out_file, sep='\t', lineterminator='\n', index=False)
+def surface(labels: np.ndarray, label: int) -> int:
+ """
+ Ad-hoc implementation for computation of the "perimeter" of an object in 3D (that is a surface).
+ """
+ assert labels.ndim == 3 # sanity check
+
+ # Create 3-D structuring element with 4-connectivity
+ selem = np.zeros((3, 3, 3), bool)
+ for ijk in np.ndindex(*selem.shape):
+ if (np.array(ijk) == 1).sum() >= 2:
+ selem[*ijk] = True # noqa: E999
+ assert selem.sum() == 7 # sanity check
+
+ # Compute the area of the surface
+ cc = (labels == label)
+ cc_interior = ndi.binary_erosion(cc, selem)
+ surface = np.logical_xor(cc, cc_interior)
+ return surface.sum() # number of voxels on the surface of the object
+
+
+def compute_if_dask(obj):
+ """
+ Return the computed object or array if it is a Dask array or deferred computable Dask object.
+ """
+ return obj.compute() if hasattr(obj, 'compute') else obj
+
+
+if __name__ == '__main__':
+ tool = giatools.ToolBaseplate()
+ tool.add_input_image('labels')
+ tool.add_input_image('intensities', required=False)
+ tool.parser.add_argument('--output', type=str)
+ tool.parse_args()
+
+ # Validate the input image
+ try:
+ label_image = tool.args.input_images['labels']
+ if any(label_image.shape[label_image.axes.index(axis)] > 1 for axis in label_image.axes if axis not in 'ZYX'):
+ raise ValueError(f'This tool is not applicable to images with {label_image.original_axes} axes.')
+
+ # Extract the image features
+ for section in tool.run('ZYX'): # the validation code above guarantees that we will have only a single iteration
+ df = pd.DataFrame()
+
+ # Get the labels array and cast to `uint8` if it is `bool` (`skimage.measure.regionprops` refuses `bool` typed arrays)
+ labels_section_data = section['labels'].data.squeeze()
+ if np.issubdtype(labels_section_data.dtype, bool):
+ print('Convert labels from bool to uint8')
+ labels_section_data = labels_section_data.astype(np.uint8)
+
+ # Some features currently cannot be computed from Dask arrays
+ if any(
+ feature_name in tool.args.params['features'] for feature_name in (
+ 'inertia_tensor_eigvals',
+ 'axis_major_length',
+ 'axis_minor_length',
+ 'eccentricity',
+ 'orientation',
+ 'moments_hu',
+ )
+ ):
+ labels_section_data = compute_if_dask(labels_section_data)
+
+ # Compute the image features
+ if 'intensities' in tool.args.input_images:
+ regions = skimage.measure.regionprops(labels_section_data, intensity_image=section['intensities'].data.squeeze())
+ else:
+ regions = skimage.measure.regionprops(labels_section_data, intensity_image=None)
+ df['it'] = np.arange(len(regions))
+ for feature_name in tool.args.params['features']:
+
+ # Add the object label
+ if feature_name == 'label':
+ df['label'] = df['it'].map(lambda ait: regions[ait].label)
+
+ # Add the object perimeter/surface
+ elif feature_name == 'perimeter' and labels_section_data.ndim == 3:
+ df['perimeter'] = df['it'].map(
+ lambda ait: surface(labels_section_data, regions[ait].label), # `skimage.measure.regionprops` cannot compute perimeters for 3-D data
+ )
+
+ # Skip features that are not available when processing 3-D images
+ elif feature_name in ('eccentricity', 'moments_hu', 'orientation') and labels_section_data.ndim == 3:
+ print(f'Skip feature that is not available for 3-D images: "{feature_name}"')
+
+ # Add another feature from `regions` that was computed via `skimage.measure.regionprops`
+ else:
+ try:
+ df[feature_name] = df['it'].map(lambda ait: getattr(regions[ait], feature_name))
+ except TypeError:
+ raise ValueError(f'Unknown feature: "{feature_name}"')
+
+ # Resolve any remaining Dask objects to the actual values (e.g., when processing Zarrs)
+ df = df.map(compute_if_dask)
+
+ # Convert lists/tuples/arrays to lists of plain Python numbers (e.g., float instead of np.float64)
+ df = df.map(
+ lambda obj: np.asarray(obj).tolist() if type(obj) in (list, tuple, np.ndarray) else obj,
+ )
+
+ del df['it']
+ df.to_csv(tool.args.raw_args.output, sep='\t', lineterminator='\n', index=False)
+
+ except ValueError as err:
+ exit(err.args[0])
diff --git a/tools/2d_feature_extraction/2d_feature_extraction.xml b/tools/2d_feature_extraction/2d_feature_extraction.xml
index 20ef5ac0f..e0c595423 100644
--- a/tools/2d_feature_extraction/2d_feature_extraction.xml
+++ b/tools/2d_feature_extraction/2d_feature_extraction.xml
@@ -2,84 +2,122 @@
with scikit-image
creators.xml
- 0.18.1
+ validators.xml
+ 0.25.2
0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
operation_3443
scikit-image
+ giatools
+ galaxy_image_analysis
scikit-image
scikit-image
pandas
- numpy
- tifffile
- giatools
+ giatools
+ ome-zarr
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -87,23 +125,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
- **Computes features of a label map.**
+**Computes features of a label map.**
+
+The computed features are computed based solely on the properties of the objects in the label map, or, optionally, by also taking the intensities from a corresponding intensity image into account. Labels with value 0 are ignored.
+
+Both images must be 2-D or 3-D single-channel images.
+
+Supported Features
+==================
+
+**Area:** Area of the region i.e. number of pixels of the region scaled by pixel-area.
+
+**Convex area:** Area of the convex hull image, which is the smallest convex polygon that encloses the region.
+
+**Filled area:** Area of the region with all the holes filled in.
+
+**Major axis length:** The length of the major axis of the ellipse that has the same normalized second central moments as the region.
+
+**Minor axis length:** The length of the minor axis of the ellipse that has the same normalized second central moments as the region.
+
+**Bounding box:** Bounding box `(min_row, min_col, max_row, max_col)`. Pixels belonging to the bounding box are in the half-open interval `[min_row; max_row)` and `[min_col; max_col)`.
+
+**Centroid:** Centroid coordinate tuple `(row, col)`.
+
+**Eccentricity:** Eccentricity of the ellipse that has the same second-moments as the region. The eccentricity is the ratio of the focal distance (distance between focal points) over the major axis length. The value is in the interval [0, 1). When it is 0, the ellipse becomes a circle.
+
+**Equivalent diameter:** The diameter of a circle with the same area as the region.
+
+**Euler number:** Euler characteristic of the set of non-zero pixels. Computed as number of connected components subtracted by number of holes (input.ndim connectivity). In 3-D, number of connected components plus number of holes subtracted by number of tunnels.
+
+**Extent:** Ratio of pixels in the region to pixels in the total bounding box. Computed as `area / (rows * cols)`.
+
+**Inertia tensor eigenvalues:** The eigenvalues of the inertia tensor in decreasing order.
+
+**Moments:** Spatial moments up to 3rd order: `m_ij = sum{ array(row, col) * row^i * col^j }` where the sum is over the row, col coordinates of the region.
+
+**Moments Hu:** Hu moments (translation, scale and rotation invariant).
+
+**Orientation:** Angle between the vertical axis (rows) and the major axis of the ellipse that has the same second moments as the region, ranging from `-pi/2` to `pi/2` counter-clockwise.
- The computed features are computed based solely on the properties of the labels in the label map,
- or, optionally, by also taking the intensities from a corresponding intensity image into account.
+**Perimeter:** Perimeter of object which approximates the contour as a line through the centers of border pixels using a 4-connectivity. Generalizes to the surface of an object in 3-D, which uses a different approximation.
- The label map must be a 2-D or 3-D single-channel image.
+**Solidity:** Ratio of pixels in the region to pixels of the convex hull image.
diff --git a/tools/2d_feature_extraction/test-data/input.tiff b/tools/2d_feature_extraction/test-data/input.tiff
deleted file mode 100644
index 91aea92a1..000000000
Binary files a/tools/2d_feature_extraction/test-data/input.tiff and /dev/null differ
diff --git a/tools/2d_feature_extraction/test-data/input/README.md b/tools/2d_feature_extraction/test-data/input/README.md
new file mode 100644
index 000000000..aef6c8ef1
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/README.md
@@ -0,0 +1,73 @@
+# Overview of the test images
+
+## Label maps
+
+### `input9.zarr`:
+
+- axes: `ZYX`
+- resolution: `(2, 100, 100)`
+- dtype: `bool`
+- binary image
+- metadata:
+ - resolution: `(1.0, 1.0)`
+ - z-spacing: `1.0`
+ - unit: `um`
+
+### `input11.tiff`
+
+- axes: `YX`
+- resolution: `(265, 329)`
+- dtype: `uint16`
+- binary image
+
+### `input12.png`
+
+- axes: `YX`
+- resolution: `(58, 64)`
+- dtype: `uint8`
+- labels: `0...24`
+
+### `input13.zarr`
+
+- axes: `YX`
+- resolution: `(265, 329)`
+- dtype: `uint8`
+- labels: `0...2`
+
+## Intensity images
+
+### `input1_uint8.tiff`:
+
+- axes: `YX`
+- resolution: `(265, 329)`
+- dtype: `uint8`
+- metadata: none
+
+### `input3_uint16.tiff`:
+
+- axes: `YXC`
+- resolution: `(58, 64, 3)`
+- dtype: `uint16`
+- metadata:
+ - resolution: `(2.0, 1.0)`
+ - unit: `mm`
+
+### `input8_zyx.zarr`:
+
+- axes: `ZYX`
+- resolution: `(2, 100, 100)`
+- dtype: `float64`
+- metadata:
+ - resolution: `(1.0, 1.0)`
+ - z-spacing: `1.0`
+ - unit: `um`
+
+### `input10.zarr`:
+
+- axes: `CYX`
+- resolution: `(2, 64, 64)`
+- dtype: `uint8`
+- metadata:
+ - resolution: `(1.0, 1.0)`
+ - z-spacing: `1.0`
+ - unit: `um`
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/0/c/0/0/0 b/tools/2d_feature_extraction/test-data/input/input10.zarr/0/c/0/0/0
new file mode 100644
index 000000000..2ef34775f
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input10.zarr/0/c/0/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/0/c/1/0/0 b/tools/2d_feature_extraction/test-data/input/input10.zarr/0/c/1/0/0
new file mode 100644
index 000000000..e2860f6a6
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input10.zarr/0/c/1/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/0/zarr.json b/tools/2d_feature_extraction/test-data/input/input10.zarr/0/zarr.json
new file mode 100644
index 000000000..d71c27690
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input10.zarr/0/zarr.json
@@ -0,0 +1,46 @@
+{
+ "shape": [
+ 2,
+ 64,
+ 64
+ ],
+ "data_type": "uint8",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 1,
+ 100,
+ 100
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": 0,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "c",
+ "y",
+ "x"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/1/c/0/0/0 b/tools/2d_feature_extraction/test-data/input/input10.zarr/1/c/0/0/0
new file mode 100644
index 000000000..33c5341dc
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input10.zarr/1/c/0/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/1/c/1/0/0 b/tools/2d_feature_extraction/test-data/input/input10.zarr/1/c/1/0/0
new file mode 100644
index 000000000..cb86b5f1c
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input10.zarr/1/c/1/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/1/zarr.json b/tools/2d_feature_extraction/test-data/input/input10.zarr/1/zarr.json
new file mode 100644
index 000000000..f60292e64
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input10.zarr/1/zarr.json
@@ -0,0 +1,46 @@
+{
+ "shape": [
+ 2,
+ 32,
+ 32
+ ],
+ "data_type": "uint8",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 1,
+ 100,
+ 100
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": 0,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "c",
+ "y",
+ "x"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/2/c/0/0/0 b/tools/2d_feature_extraction/test-data/input/input10.zarr/2/c/0/0/0
new file mode 100644
index 000000000..278b9f4bf
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input10.zarr/2/c/0/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/2/c/1/0/0 b/tools/2d_feature_extraction/test-data/input/input10.zarr/2/c/1/0/0
new file mode 100644
index 000000000..6cedb9a26
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input10.zarr/2/c/1/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/2/zarr.json b/tools/2d_feature_extraction/test-data/input/input10.zarr/2/zarr.json
new file mode 100644
index 000000000..7c0d81839
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input10.zarr/2/zarr.json
@@ -0,0 +1,46 @@
+{
+ "shape": [
+ 2,
+ 16,
+ 16
+ ],
+ "data_type": "uint8",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 1,
+ 100,
+ 100
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": 0,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "c",
+ "y",
+ "x"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/3/c/0/0/0 b/tools/2d_feature_extraction/test-data/input/input10.zarr/3/c/0/0/0
new file mode 100644
index 000000000..dd9423634
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input10.zarr/3/c/0/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/3/c/1/0/0 b/tools/2d_feature_extraction/test-data/input/input10.zarr/3/c/1/0/0
new file mode 100644
index 000000000..089f8dac2
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input10.zarr/3/c/1/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/3/zarr.json b/tools/2d_feature_extraction/test-data/input/input10.zarr/3/zarr.json
new file mode 100644
index 000000000..2055db168
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input10.zarr/3/zarr.json
@@ -0,0 +1,46 @@
+{
+ "shape": [
+ 2,
+ 8,
+ 8
+ ],
+ "data_type": "uint8",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 1,
+ 100,
+ 100
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": 0,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "c",
+ "y",
+ "x"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/4/c/1/0/0 b/tools/2d_feature_extraction/test-data/input/input10.zarr/4/c/1/0/0
new file mode 100644
index 000000000..2df69a708
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input10.zarr/4/c/1/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/4/zarr.json b/tools/2d_feature_extraction/test-data/input/input10.zarr/4/zarr.json
new file mode 100644
index 000000000..f56724b79
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input10.zarr/4/zarr.json
@@ -0,0 +1,46 @@
+{
+ "shape": [
+ 2,
+ 4,
+ 4
+ ],
+ "data_type": "uint8",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 1,
+ 100,
+ 100
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": 0,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "c",
+ "y",
+ "x"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input10.zarr/zarr.json b/tools/2d_feature_extraction/test-data/input/input10.zarr/zarr.json
new file mode 100644
index 000000000..107c0f027
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input10.zarr/zarr.json
@@ -0,0 +1,95 @@
+{
+ "attributes": {
+ "ome": {
+ "version": "0.5",
+ "multiscales": [
+ {
+ "datasets": [
+ {
+ "path": "0",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ }
+ ]
+ },
+ {
+ "path": "1",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 2.0,
+ 2.0
+ ]
+ }
+ ]
+ },
+ {
+ "path": "2",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 4.0,
+ 4.0
+ ]
+ }
+ ]
+ },
+ {
+ "path": "3",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 8.0,
+ 8.0
+ ]
+ }
+ ]
+ },
+ {
+ "path": "4",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 16.0,
+ 16.0
+ ]
+ }
+ ]
+ }
+ ],
+ "name": "/",
+ "axes": [
+ {
+ "name": "c",
+ "type": "channel"
+ },
+ {
+ "name": "y",
+ "type": "space"
+ },
+ {
+ "name": "x",
+ "type": "space"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "zarr_format": 3,
+ "node_type": "group"
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input11.tiff b/tools/2d_feature_extraction/test-data/input/input11.tiff
new file mode 100644
index 000000000..b2afd0a68
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input11.tiff differ
diff --git a/tools/2d_feature_extraction/test-data/input/input12.png b/tools/2d_feature_extraction/test-data/input/input12.png
new file mode 100644
index 000000000..d5082ecd9
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input12.png differ
diff --git a/tools/2d_feature_extraction/test-data/input/input13.zarr/0/c/0/0 b/tools/2d_feature_extraction/test-data/input/input13.zarr/0/c/0/0
new file mode 100644
index 000000000..494658b65
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input13.zarr/0/c/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input13.zarr/0/zarr.json b/tools/2d_feature_extraction/test-data/input/input13.zarr/0/zarr.json
new file mode 100644
index 000000000..f2dd2c325
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input13.zarr/0/zarr.json
@@ -0,0 +1,43 @@
+{
+ "shape": [
+ 265,
+ 329
+ ],
+ "data_type": "uint8",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 265,
+ 329
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": 0,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "Y",
+ "X"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input13.zarr/zarr.json b/tools/2d_feature_extraction/test-data/input/input13.zarr/zarr.json
new file mode 100644
index 000000000..c49cd690b
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input13.zarr/zarr.json
@@ -0,0 +1,38 @@
+{
+ "attributes": {
+ "ome": {
+ "version": "0.5",
+ "multiscales": [
+ {
+ "datasets": [
+ {
+ "path": "0",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 1.0
+ ]
+ }
+ ]
+ }
+ ],
+ "name": "/",
+ "axes": [
+ {
+ "name": "Y",
+ "type": "space"
+ },
+ {
+ "name": "X",
+ "type": "space"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "zarr_format": 3,
+ "node_type": "group"
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input1_uint8.tiff b/tools/2d_feature_extraction/test-data/input/input1_uint8.tiff
new file mode 100644
index 000000000..0d1b003b4
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input1_uint8.tiff differ
diff --git a/tools/2d_feature_extraction/test-data/input/input3_uint16.tiff b/tools/2d_feature_extraction/test-data/input/input3_uint16.tiff
new file mode 100644
index 000000000..a78268fb1
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input3_uint16.tiff differ
diff --git a/tools/2d_feature_extraction/test-data/input/input8_zyx.zarr b/tools/2d_feature_extraction/test-data/input/input8_zyx.zarr
new file mode 120000
index 000000000..7af6099fa
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input8_zyx.zarr
@@ -0,0 +1 @@
+ome-zarr-examples/image-03.zarr
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/0/c/0/0/0 b/tools/2d_feature_extraction/test-data/input/input9.zarr/0/c/0/0/0
new file mode 100644
index 000000000..0a7819de0
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input9.zarr/0/c/0/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/0/c/1/0/0 b/tools/2d_feature_extraction/test-data/input/input9.zarr/0/c/1/0/0
new file mode 100644
index 000000000..fef65d096
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input9.zarr/0/c/1/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/0/zarr.json b/tools/2d_feature_extraction/test-data/input/input9.zarr/0/zarr.json
new file mode 100644
index 000000000..a42884236
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input9.zarr/0/zarr.json
@@ -0,0 +1,46 @@
+{
+ "shape": [
+ 2,
+ 100,
+ 100
+ ],
+ "data_type": "bool",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 1,
+ 100,
+ 100
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": false,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "z",
+ "y",
+ "x"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/1/c/0/0/0 b/tools/2d_feature_extraction/test-data/input/input9.zarr/1/c/0/0/0
new file mode 100644
index 000000000..0c9f3c08b
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input9.zarr/1/c/0/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/1/c/1/0/0 b/tools/2d_feature_extraction/test-data/input/input9.zarr/1/c/1/0/0
new file mode 100644
index 000000000..f58c34bd8
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input9.zarr/1/c/1/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/1/zarr.json b/tools/2d_feature_extraction/test-data/input/input9.zarr/1/zarr.json
new file mode 100644
index 000000000..548820b7d
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input9.zarr/1/zarr.json
@@ -0,0 +1,46 @@
+{
+ "shape": [
+ 2,
+ 50,
+ 50
+ ],
+ "data_type": "bool",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 1,
+ 100,
+ 100
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": false,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "z",
+ "y",
+ "x"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/2/c/0/0/0 b/tools/2d_feature_extraction/test-data/input/input9.zarr/2/c/0/0/0
new file mode 100644
index 000000000..caf7b4257
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input9.zarr/2/c/0/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/2/c/1/0/0 b/tools/2d_feature_extraction/test-data/input/input9.zarr/2/c/1/0/0
new file mode 100644
index 000000000..f5331c9a9
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input9.zarr/2/c/1/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/2/zarr.json b/tools/2d_feature_extraction/test-data/input/input9.zarr/2/zarr.json
new file mode 100644
index 000000000..dc4b3315d
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input9.zarr/2/zarr.json
@@ -0,0 +1,46 @@
+{
+ "shape": [
+ 2,
+ 25,
+ 25
+ ],
+ "data_type": "bool",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 1,
+ 100,
+ 100
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": false,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "z",
+ "y",
+ "x"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/3/c/0/0/0 b/tools/2d_feature_extraction/test-data/input/input9.zarr/3/c/0/0/0
new file mode 100644
index 000000000..a33bd7699
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input9.zarr/3/c/0/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/3/c/1/0/0 b/tools/2d_feature_extraction/test-data/input/input9.zarr/3/c/1/0/0
new file mode 100644
index 000000000..ffa5f4c0e
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input9.zarr/3/c/1/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/3/zarr.json b/tools/2d_feature_extraction/test-data/input/input9.zarr/3/zarr.json
new file mode 100644
index 000000000..6e41e8dc3
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input9.zarr/3/zarr.json
@@ -0,0 +1,46 @@
+{
+ "shape": [
+ 2,
+ 12,
+ 12
+ ],
+ "data_type": "bool",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 1,
+ 100,
+ 100
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": false,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "z",
+ "y",
+ "x"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/4/c/0/0/0 b/tools/2d_feature_extraction/test-data/input/input9.zarr/4/c/0/0/0
new file mode 100644
index 000000000..f3eedd45a
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input9.zarr/4/c/0/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/4/c/1/0/0 b/tools/2d_feature_extraction/test-data/input/input9.zarr/4/c/1/0/0
new file mode 100644
index 000000000..4aa4866f0
Binary files /dev/null and b/tools/2d_feature_extraction/test-data/input/input9.zarr/4/c/1/0/0 differ
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/4/zarr.json b/tools/2d_feature_extraction/test-data/input/input9.zarr/4/zarr.json
new file mode 100644
index 000000000..266304c53
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input9.zarr/4/zarr.json
@@ -0,0 +1,46 @@
+{
+ "shape": [
+ 2,
+ 6,
+ 6
+ ],
+ "data_type": "bool",
+ "chunk_grid": {
+ "name": "regular",
+ "configuration": {
+ "chunk_shape": [
+ 1,
+ 100,
+ 100
+ ]
+ }
+ },
+ "chunk_key_encoding": {
+ "name": "default",
+ "configuration": {
+ "separator": "/"
+ }
+ },
+ "fill_value": false,
+ "codecs": [
+ {
+ "name": "bytes"
+ },
+ {
+ "name": "zstd",
+ "configuration": {
+ "level": 0,
+ "checksum": false
+ }
+ }
+ ],
+ "attributes": {},
+ "dimension_names": [
+ "z",
+ "y",
+ "x"
+ ],
+ "zarr_format": 3,
+ "node_type": "array",
+ "storage_transformers": []
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/input9.zarr/zarr.json b/tools/2d_feature_extraction/test-data/input/input9.zarr/zarr.json
new file mode 100644
index 000000000..b96da58f9
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/input9.zarr/zarr.json
@@ -0,0 +1,98 @@
+{
+ "attributes": {
+ "ome": {
+ "version": "0.5",
+ "multiscales": [
+ {
+ "datasets": [
+ {
+ "path": "0",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 1.0,
+ 1.0
+ ]
+ }
+ ]
+ },
+ {
+ "path": "1",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 2.0,
+ 2.0
+ ]
+ }
+ ]
+ },
+ {
+ "path": "2",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 4.0,
+ 4.0
+ ]
+ }
+ ]
+ },
+ {
+ "path": "3",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 8.333333333333334,
+ 8.333333333333334
+ ]
+ }
+ ]
+ },
+ {
+ "path": "4",
+ "coordinateTransformations": [
+ {
+ "type": "scale",
+ "scale": [
+ 1.0,
+ 16.666666666666668,
+ 16.666666666666668
+ ]
+ }
+ ]
+ }
+ ],
+ "name": "/",
+ "axes": [
+ {
+ "name": "z",
+ "type": "space",
+ "unit": "micrometer"
+ },
+ {
+ "name": "y",
+ "type": "space",
+ "unit": "micrometer"
+ },
+ {
+ "name": "x",
+ "type": "space",
+ "unit": "micrometer"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "zarr_format": 3,
+ "node_type": "group"
+}
diff --git a/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/LICENSE b/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/LICENSE
new file mode 100644
index 000000000..fe10a3ceb
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/LICENSE
@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2023, Tommaso Comparin
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/image-03.zarr/.zattrs b/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/image-03.zarr/.zattrs
new file mode 100644
index 000000000..b8cfbbd17
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/image-03.zarr/.zattrs
@@ -0,0 +1,40 @@
+{
+ "multiscales": [
+ {
+ "axes": [
+ {
+ "name": "z",
+ "type": "space",
+ "unit": "micrometer"
+ },
+ {
+ "name": "y",
+ "type": "space",
+ "unit": "micrometer"
+ },
+ {
+ "name": "x",
+ "type": "space",
+ "unit": "micrometer"
+ }
+ ],
+ "datasets": [
+ {
+ "coordinateTransformations": [
+ {
+ "scale": [
+ 1.0,
+ 1.0,
+ 1.0
+ ],
+ "type": "scale"
+ }
+ ],
+ "path": "0"
+ }
+ ],
+ "version": "0.4"
+ }
+ ],
+ "version": "0.4"
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/image-03.zarr/.zgroup b/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/image-03.zarr/.zgroup
new file mode 100644
index 000000000..3b7daf227
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/image-03.zarr/.zgroup
@@ -0,0 +1,3 @@
+{
+ "zarr_format": 2
+}
\ No newline at end of file
diff --git a/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/image-03.zarr/0/.zarray b/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/image-03.zarr/0/.zarray
new file mode 100644
index 000000000..37c8bbba3
--- /dev/null
+++ b/tools/2d_feature_extraction/test-data/input/ome-zarr-examples/image-03.zarr/0/.zarray
@@ -0,0 +1,25 @@
+{
+ "chunks": [
+ 1,
+ 50,
+ 100
+ ],
+ "compressor": {
+ "blocksize": 0,
+ "clevel": 5,
+ "cname": "lz4",
+ "id": "blosc",
+ "shuffle": 1
+ },
+ "dimension_separator": "/",
+ "dtype": "