Skip to content

Commit 25ec2fa

Browse files
committed
Merge remote-tracking branch 'origin/main' into validator-framework
2 parents 0e61b87 + 4be616b commit 25ec2fa

31 files changed

+2758
-337
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Put an x in the boxes that apply. You can also fill these out after creating the
2323

2424
## If you changed the specification
2525
- [ ] I have checked that any validation functions and tests reflect the changes.
26-
- [ ] I have updated the GeffMetadata and the json schema using `pixi run update-schema` if necessary.
26+
- [ ] I have updated the GeffMetadata and the json schema using `pixi run update-json` if necessary.
2727
- [ ] I have updated docs/specification.md to reflect the change.
2828
- [ ] I have updated implementations to reflect the change. (This can happen in separate PRs on a feature branch, but must be complete before merging into main.)
2929

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ wheels/
1212

1313
# Virtual environments
1414
.venv
15+
.python-version
1516

1617
# pixi environments
1718
.pixi
@@ -29,4 +30,5 @@ coverage.xml
2930

3031
uv.lock
3132
/.idea
33+
.vscode/
3234
site

docs/specification.md

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ Currently, `geff` supports zarr specifications [2](https://zarr-specs.readthedoc
2222

2323
::: geff.units.VALID_TIME_UNITS
2424

25+
### Affine transformations
26+
The optional `affine` field allows specifying a global affine transformation that maps the graph coordinates stored in the node properties to a physical coordinate system. The value **matrix** is stored as a `(N + 1) × (N + 1)` homogeneous matrix following the `scipy.ndimage.affine_transform` convention, where **N** equals the number of spatio-temporal axes declared in `axes`.
27+
28+
### Extra attributes
29+
30+
The optional `extra` object is a free-form dictionary that can hold any additional, application-specific metadata that is **not** covered by the core geff schema. Users may place arbitrary keys and values inside `extra` without fear of clashing with future reserved fields. Although the core `geff` reader makes these attributes available, their meaning and use are left entirely to downstream applications.
31+
2532
## The `nodes` group
2633
The nodes group will contain an `ids` array and optionally a `props` group.
2734
### The `ids` array
@@ -37,6 +44,10 @@ The `nodes\props` group is optional and will contain one or more `node property`
3744
- Geff provides special support for spatio-temporal properties, although they are not required. When `axes` are specified in the `geff` metadata, each axis name identifies a spatio-temporal property. Spatio-temporal properties are not allowed to have missing arrays. Otherwise, they are identical to other properties from a storage specification perspective.
3845

3946
- The `seg_id` property is an optional, special node property that stores the segmenatation label for each node. The `seg_id` values do not need to be unique, in case labels are repeated between time points. If the `seg_id` property is not present, it is assumed that the graph is not associated with a segmentation.
47+
48+
- Geff provides special support for predefined shape properties, although they are not required. These currently include: `sphere`, `ellipsoid`. Values can be marked as `missing`, and a geff graph may contain multiple different shape properties. Units of shapes are assumed to be the same as the units on the spatial axes. Otherwise, shape properties are identical to other properties from a storage specification perspective.
49+
- `sphere`: Hypersphere in n spatial dimensions, defined by a scalar radius.
50+
- `ellipsoid`: Defined by a symmetric positive-definite covariance matrix, whose dimensionality is assumed to match the spatial axes.
4051
<!-- Perhaps we just let the user specify the seg id property in the metadata instead? Then you can point it to the node ids if you wanted to -->
4152

4253
!!! note
@@ -76,6 +87,12 @@ Here is a schematic of the expected file structure.
7687
values # shape: (N,) dtype: float32
7788
x/
7889
values # shape: (N,) dtype: float32
90+
radius/
91+
values # shape: (N,) dtype: int | float
92+
missing # shape: (N,) dtype: bool
93+
covariance3d/
94+
values # shape: (N, 3, 3) dtype: float
95+
missing # shape: (N,) dtype: bool
7996
color/
8097
values # shape: (N, 4) dtype: float16
8198
missing # shape: (N,) dtype: bool
@@ -105,8 +122,41 @@ This is a geff metadata zattrs file that matches the above example structure.
105122
{'name': 'z', 'type': "space", 'unit': "micrometers", 'min': 1523.36, 'max': 4398.1},
106123
{'name': 'y', 'type': "space", 'unit': "micrometers", 'min': 81.667, 'max': 1877.7},
107124
{'name': 'x', 'type': "space", 'unit': "micrometers", 'min': 764.42, 'max': 2152.3},
108-
]
125+
],
126+
# predefined node attributes for storing detections as spheres or ellipsoids
127+
"sphere": "radius", # optional
128+
"ellipsoid": "covariance3d", # optional
129+
"display_hints": {
130+
"display_horizontal": "x",
131+
"display_vertical": "y",
132+
"display_depth": "z",
133+
"display_time": "t",
134+
},
135+
# node attributes corresponding to tracklet and/or lineage IDs
136+
"track_node_props": {
137+
"lineage": "ultrack_lineage_id",
138+
"tracklet": "ultrack_id"
139+
},
140+
"related_objects": {
141+
{
142+
"type":"labels", "path":"../segmentation/", "label_prop": "seg_id",
143+
},
144+
{
145+
"type":"image", "path":"../raw/",
146+
},
147+
},
148+
# optional coordinate transformation is defined as homogeneous coordinates
149+
# It is expected to be a (D+1)x(D+1) matrix where D is the number of axes
150+
"affine": [
151+
[1, 0, 0, 0, 0],
152+
[0, 1, 0, 0, 0],
153+
[0, 0, 1, 0, 0],
154+
[0, 0, 0, 1, 0],
155+
[0, 0, 0, 0, 1],
156+
# custom other things must be placed **inside** the extra attribute
157+
"extra": {
158+
...
159+
}
109160
}
110-
... # custom other things are allowed and ignored by geff
111161
}
112162
```

geff-schema.json

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
{
22
"$defs": {
3+
"Affine": {
4+
"description": "Affine transformation class following scipy conventions.\n\nInternally stores transformations as homogeneous coordinate matrices (N+1, N+1).\nThe transformation matrix follows scipy.ndimage.affine_transform convention\nwhere the matrix maps output coordinates to input coordinates (inverse/pull transformation).\n\nFor a point p_out in output space, the corresponding input point p_in is computed as:\np_in_homo = matrix @ p_out_homo\nwhere p_out_homo = [p_out; 1] and p_in = p_in_homo[:-1]\n\nAttributes:\n matrix: Homogeneous transformation matrix as list of lists (ndim+1, ndim+1)",
5+
"properties": {
6+
"matrix": {
7+
"description": "Homogeneous transformation matrix as list of lists (ndim+1, ndim+1)",
8+
"title": "Matrix"
9+
}
10+
},
11+
"required": [
12+
"matrix"
13+
],
14+
"title": "Affine",
15+
"type": "object"
16+
},
317
"Axis": {
418
"properties": {
519
"name": {
@@ -61,6 +75,53 @@
6175
"title": "Axis",
6276
"type": "object"
6377
},
78+
"DisplayHint": {
79+
"description": "Metadata indicating how spatiotemporal axes are displayed by a viewer",
80+
"properties": {
81+
"display_horizontal": {
82+
"description": "Which spatial axis to use for horizontal display",
83+
"title": "Display Horizontal",
84+
"type": "string"
85+
},
86+
"display_vertical": {
87+
"description": "Which spatial axis to use for vertical display",
88+
"title": "Display Vertical",
89+
"type": "string"
90+
},
91+
"display_depth": {
92+
"anyOf": [
93+
{
94+
"type": "string"
95+
},
96+
{
97+
"type": "null"
98+
}
99+
],
100+
"default": null,
101+
"description": "Optional, which spatial axis to use for depth display",
102+
"title": "Display Depth"
103+
},
104+
"display_time": {
105+
"anyOf": [
106+
{
107+
"type": "string"
108+
},
109+
{
110+
"type": "null"
111+
}
112+
],
113+
"default": null,
114+
"description": "Optional, which temporal axis to use for time",
115+
"title": "Display Time"
116+
}
117+
},
118+
"required": [
119+
"display_horizontal",
120+
"display_vertical"
121+
],
122+
"title": "DisplayHint",
123+
"type": "object"
124+
},
64125
"GeffMetadata": {
65126
"description": "Geff metadata schema to validate the attributes json file in a geff zarr",
66127
"properties": {
@@ -90,6 +151,98 @@
90151
"default": null,
91152
"description": "Optional list of Axis objects defining the axes of each node in the graph.\nEach object's `name` must be an existing attribute on the nodes. The optional `type` keymust be one of `space`, `time` or `channel`, though readers may not use this information. Each axis can additionally optionally define a `unit` key, which should match the validOME-Zarr units, and `min` and `max` keys to define the range of the axis.",
92153
"title": "Axes"
154+
},
155+
"sphere": {
156+
"anyOf": [
157+
{
158+
"type": "string"
159+
},
160+
{
161+
"type": "null"
162+
}
163+
],
164+
"default": null,
165+
"description": "\n Name of the optional `sphere` property.\n\n A sphere is defined by\n - a center point, already given by the `space` type properties\n - a radius scalar, stored in this property\n ",
166+
"title": "Node property: Detections as spheres"
167+
},
168+
"ellipsoid": {
169+
"anyOf": [
170+
{
171+
"type": "string"
172+
},
173+
{
174+
"type": "null"
175+
}
176+
],
177+
"default": null,
178+
"description": "\n Name of the `ellipsoid` property.\n\n An ellipsoid is assumed to be in the same coordinate system as the `space` type\n properties.\n\n It is defined by\n - a center point :math:`c`, already given by the `space` type properties\n - a covariance matrix :math:`\\Sigma`, symmetric and positive-definite, stored in this\n property as a `2x2`/`3x3` array.\n\n To plot the ellipsoid:\n - Compute the eigendecomposition of the covariance matrix\n :math:`\\Sigma = Q \\Lambda Q^{\\top}`\n - Sample points :math:`z` on the unit sphere\n - Transform the points to the ellipsoid by\n :math:`x = c + Q \\Lambda^{(1/2)} z`.\n ",
179+
"title": "Node property: Detections as ellipsoids"
180+
},
181+
"track_node_props": {
182+
"anyOf": [
183+
{
184+
"additionalProperties": {
185+
"type": "string"
186+
},
187+
"propertyNames": {
188+
"enum": [
189+
"lineage",
190+
"tracklet"
191+
]
192+
},
193+
"type": "object"
194+
},
195+
{
196+
"type": "null"
197+
}
198+
],
199+
"default": null,
200+
"description": "Node properties denoting tracklet and/or lineage IDs.\nA tracklet is defined as a simple path of connected nodes where the initiating node has any incoming degree and outgoing degree at most 1,and the terminating node has incoming degree at most 1 and any outgoing degree, and other nodes along the path have in/out degree of 1. Each tracklet must contain the maximal set of connected nodes that match this definition - no sub-tracklets.\nA lineage is defined as a weakly connected component on the graph.\nThe dictionary can store one or both of 'tracklet' or 'lineage' keys.",
201+
"title": "Track Node Props"
202+
},
203+
"related_objects": {
204+
"anyOf": [
205+
{
206+
"items": {
207+
"$ref": "#/$defs/RelatedObject"
208+
},
209+
"type": "array"
210+
},
211+
{
212+
"type": "null"
213+
}
214+
],
215+
"default": null,
216+
"description": "A list of dictionaries of related objects such as labels or images. Each dictionary must contain 'type', 'path', and optionally 'label_prop' properties. The 'type' represents the data type. 'labels' and 'image' should be used for label and image objects, respectively. Other types are also allowed, The 'path' should be relative to the geff zarr-attributes file. It is strongly recommended all related objects are stored as siblings of the geff group within the top-level zarr group. The 'label_prop' is only valid for type 'labels' and specifies the node property that will be used to identify the labels in the related object. ",
217+
"title": "Related Objects"
218+
},
219+
"affine": {
220+
"anyOf": [
221+
{
222+
"$ref": "#/$defs/Affine"
223+
},
224+
{
225+
"type": "null"
226+
}
227+
],
228+
"default": null,
229+
"description": "Affine transformation matrix to transform the graph coordinates to the physical coordinates. The matrix must have the same number of dimensions as the number of axes in the graph."
230+
},
231+
"display_hints": {
232+
"anyOf": [
233+
{
234+
"$ref": "#/$defs/DisplayHint"
235+
},
236+
{
237+
"type": "null"
238+
}
239+
],
240+
"default": null,
241+
"description": "Metadata indicating how spatiotemporal axes are displayed by a viewer"
242+
},
243+
"extra": {
244+
"description": "Extra metadata that is not part of the schema",
245+
"title": "Extra"
93246
}
94247
},
95248
"required": [
@@ -98,6 +251,39 @@
98251
],
99252
"title": "geff_metadata",
100253
"type": "object"
254+
},
255+
"RelatedObject": {
256+
"properties": {
257+
"type": {
258+
"description": "Type of the related object. 'labels' for label objects, 'image' for image objects. Other types are also allowed, but may not be recognized by reader applications. ",
259+
"title": "Type",
260+
"type": "string"
261+
},
262+
"path": {
263+
"description": "Path of the related object within the zarr group, relative to the geff zarr-attributes file. It is strongly recommended all related objects are stored as siblings of the geff group within the top-level zarr group.",
264+
"title": "Path",
265+
"type": "string"
266+
},
267+
"label_prop": {
268+
"anyOf": [
269+
{
270+
"type": "string"
271+
},
272+
{
273+
"type": "null"
274+
}
275+
],
276+
"default": null,
277+
"description": "Property name for label objects. This is the node property that will be used to identify the labels in the related object. This is only valid for type 'labels'.",
278+
"title": "Label Prop"
279+
}
280+
},
281+
"required": [
282+
"type",
283+
"path"
284+
],
285+
"title": "RelatedObject",
286+
"type": "object"
101287
}
102288
},
103289
"properties": {

pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ requires-python = ">=3.10"
1212
license = { text = "MIT License" }
1313
dynamic = ['version']
1414
dependencies = [
15+
"typer",
1516
"zarr>2,<4",
1617
"pydantic>=2",
1718
"numcodecs<0.16", # TODO: remove pin once the 16 release is stable
@@ -31,6 +32,7 @@ classifiers = [
3132

3233
[project.optional-dependencies]
3334
spatial-graph = ["spatial-graph"]
35+
ctc = ["dask", "scikit-image", "tifffile", "imagecodecs"]
3436

3537
[dependency-groups]
3638
test = ["pytest>=8.3.4", "pytest-cov>=6.2"]
@@ -42,6 +44,7 @@ dev = [
4244
"ipython",
4345
"types-PyYAML",
4446
"spatial-graph",
47+
"geff[ctc]"
4548
]
4649
docs = [
4750
"mkdocs-material",
@@ -83,6 +86,7 @@ update-json = "python scripts/export_json_schema.py"
8386

8487
[tool.pixi.feature.dev.tasks]
8588
test = "coverage run -m pytest"
89+
test-cov = "coverage run -m pytest && coverage xml && coverage report --show-missing"
8690
benchmark = { cmd = "pytest tests/bench.py" }
8791

8892
[tool.pixi.feature.bench.tasks]
@@ -189,5 +193,5 @@ extend-ignore-identifiers-re = ["(?i)ome"]
189193
# "tests/**/*",
190194
# ]
191195

192-
[project.entry-points.pytest11]
193-
geff = "geff._pytest_plugin"
196+
[project.entry-points.console_scripts]
197+
ctc2geff = "geff.interops.ctc:app"

0 commit comments

Comments
 (0)