Closed campagnola closed 4 years ago
Ok, the hackathon went well and we had many productive discussions. Here I will describe the plan that we came up with and that was presented to the group at the end of the hackathon:
Addition of a CoordinateSystem
class that allows declaration of coordinate systems that are used within the NWB:
CoordinateSystem
name: string
description: string
dimensions: integer number of dimensions, usually 2 or 3.
units: list of strings describing the unit of length along each axis of the
coordinate system
transforms: list of transforms that link this coordinate system to others defined
either in the same file, or to external, standardized coordinate systems such
as Allen Institute's CCF.
Addition of a /general/coordinate_systems
group that contains instances of CoordinateSystem
. Most NWB files would contain a small number (perhaps 0-2) of CoordinateSystem
instances; a very common use case might be to have a single CoordinateSystem
named "global" to which all other data in the file can be related. In some cases, however, it would be useful to declare multiple coordinate systems that are either linked by transforms (meaning we can map spatial coordinates from one CS to the other), or that are independent (we cannot map coordinates between these).
Addition of a TransformBase
class and a collection of subclasses implementing commonly-used transforms:
TransformBase
source_cs
destination_cs
TranslateTransform
offset
source_cs
destination_cs
AffineTransform
matrix
offset
source_cs
destination_cs
Each transform specifies the coordinate system that it maps from (source_cs) and to (destination_cs), and also carries any parameters needed to mathematically specify the transformation. destination_cs
could be either one of the coordinate systems defined in /general/coordinate_systems
, or a reference to an external / standardized coordinate system.
Anywhere we store spatial data (such as in the electrode positions in an electrode table, or in a SpatialSeries
or in the pixel locations in an image), it must be possible to unambiguously define the meaning of these coordinates. Each set of coordinates implicitly defines a coordinate system, so we seek to make this definition more explicit. This is done primarily by the addition of a transform
subgroup wherever spatial data is found:
Image.transform
and ImageSeries.transform
would each contain a single instance of a transform with a destination_cs
that references one of the entries defined in /general/coordinate_systems
. The source_cs
parameter would implicitly refer to the pixel/voxel coordinate system of the parent image. That is, the transform maps from pixel/voxel index (row, column) or (frame, row, column) back to the destination_cs
. Electrode.transform
would map from the coordinates used in the electrode table to one of the entries in /general_coordinate_systems
. For clarity, a set of standard conventions should be adopted that determine the meaning of the implicit source_cs
used by Electrode.transform
-- for example, a single-shank electrode might define this coordinate system as having its origin at the tip of the shank, and its +x axis pointing along the shank, in the direction of the tip. Similar conventions should be adopted for other types of electrode.SpatialSeries.transform
would be used as a more general approach, and it would be up to the experimenter to describe the meaning of the source_cs
(although this would not always be necessary; sometimes just having the transformation is sufficient).Note: In the current schema, it is possible to overspecify relationships between coordinate systems (for example, there may be multiple ways to map from one CS to another, and thus the mapping could be ambiguous depending on the path chosen. This could be mitigated in the Python API by detecting / warning about such conditions, or by refusing to allow them.
/general/coordinate_systems
can be accessed from the NWB file:
global_cs = nwb_file.coordinate_systems['global']
image = ImageSeries(...)
image.transform = AffineTransform(matrix, offset, destination_cs=global_cs)
global_pts = image.transform.map(pixel_indices)
pixel_indices = image.transform.imap(global_pts)
Transforms can be automatically composed together in order to map between any two coordinate systems, as long as they are linked in some way:
image1.transform = AffineTransform(matrix1, offset1, destination_cs=global_cs)
image2.transform = AffineTransform(matrix2, offset2, destination_cs=global_cs)
# Automatically join together [image2.transform.inverse, image1.transform]
composed_transform = image1.pixel_cs.transform_to(image2.pixel_cs)
# Map pixel location from one image to another
pixel_inds2 = composed_transform.map(pixel_inds1)
# Alternatively, these could be combined into one line:
pixel_inds2 = image1.pixel_cs.map_to(image2.pixel_cs, pixel_inds1)
ccf_pos = image.pixel_cs.map_to("AICCF", pixel_inds)
Most of the python transform infrastructure is being developed as an independent package: https://github.com/campagnola/transformy
@DavidTingley @samamckenzie
We're closing this issue for now, but it is closely related to NWB-related ontologies work being done by @lydiang.
I see good effort here to allow data to be temporally aligned, but there are many types of experiment where spatial alignment is crucial, and (as far as I can tell) this does not appear to be well supported by the spec or API.
Example use cases:
Suggested implementation details: