Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ For imaging data, you can use `AnatomicalCoordinatesImage` to store anatomical c
This is useful when you want to localize a field of view or register imaging data to a reference atlas.

Each `AnatomicalCoordinatesImage` requires:
- An `Image` object (required) — the reference image (e.g. mean or max projection) on which the coordinate map is based
- A `localized_entity` (optional) — a `OnePhotonSeries` or `TwoPhotonSeries` that this coordinate map applies to
- An `Image` object (optional) — the reference image (e.g. mean or max projection) on which the coordinate map is based
- A `localized_entity` (optional) — a `ImagingPlane` associated with the OnePhotonSeries or TwoPhotonSeries that this coordinate map applies t

The x, y, and z datasets store 2D arrays of coordinates for each pixel in the image, x[i, j], y[i, j], z[i, j] give the anatomical coordinates location for pixel (i, j).
The `get_coordinates()` function return the image with anatomical coordinates per pixel:
Expand Down Expand Up @@ -209,7 +209,7 @@ localization.add_anatomical_coordinates_tables([table])

```python
from pynwb.testing.mock.file import mock_NWBFile
from pynwb.testing.mock.ophys import mock_TwoPhotonSeries
from pynwb.testing.mock.ophys import mock_ImagingPlane
from pynwb.base import Images
from pynwb.image import GrayscaleImage
import numpy as np
Expand All @@ -228,15 +228,15 @@ image_collection = nwbfile.processing["ophys"].data_interfaces["SummaryImages"]
image_collection.add_image(GrayscaleImage(name="MeanImage", data=np.ones((512, 512)), description="Mean projection"))

# The recording series this coordinate map applies to
two_photon_series = mock_TwoPhotonSeries(nwbfile=nwbfile, name="TwoPhotonSeries")
imaging_plane = mock_ImagingPlane(nwbfile=nwbfile, name="MyImagingPlane")

space = AllenCCFv3Space()
localization.add_spaces([space])

image_coordinates = AnatomicalCoordinatesImage(
name="MyAnatomicalLocalization",
image=image_collection["MeanImage"],
localized_entity=two_photon_series,
localized_entity=imaging_plane,
method="manual registration",
space=space,
x=np.ones((512, 512)),
Expand Down
18 changes: 7 additions & 11 deletions spec/ndx-anatomical-localization.extensions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ groups:
doc: "Orientation of the coordinate system (fixed to RAS, indicating positive axis directions)"
- neurodata_type_def: NMTv2Space
neurodata_type_inc: Space
doc:
"The NIMH Macaque Template version 2.0 symmetric (Jung et al. 2021).
doc: "The NIMH Macaque Template version 2.0 symmetric (Jung et al. 2021).
This canonical space uses RAS orientation (positive x=Right, positive y=Anterior, positive z=Superior)
with millimeter units. The origin (0,0,0) is at ear bar zero."
attributes:
Expand All @@ -136,8 +135,7 @@ groups:
doc: "Orientation of the coordinate system (fixed to RAS, indicating positive axis directions)"
- neurodata_type_def: NMTv2AsymmetricSpace
neurodata_type_inc: Space
doc:
"The NIMH Macaque Template version 2.0 asymmetric (Jung et al. 2021).
doc: "The NIMH Macaque Template version 2.0 asymmetric (Jung et al. 2021).
This canonical space uses RAS orientation (positive x=Right, positive y=Anterior, positive z=Superior)
with millimeter units. The origin (0,0,0) is at ear bar zero. Unlike the symmetric variant,
this template preserves population-level hemispheric differences."
Expand All @@ -160,8 +158,7 @@ groups:
doc: "Orientation of the coordinate system (fixed to RAS, indicating positive axis directions)"
- neurodata_type_def: MEBRAINSSpace
neurodata_type_inc: Space
doc:
"The MEBRAINS macaque brain atlas version 1.0 (Balan et al. 2024).
doc: "The MEBRAINS macaque brain atlas version 1.0 (Balan et al. 2024).
This canonical space uses RAS orientation (positive x=Right, positive y=Anterior, positive z=Superior)
with millimeter units. The origin (0,0,0) is at the anterior commissure."
attributes:
Expand Down Expand Up @@ -226,12 +223,12 @@ groups:
doc: "The space in which the coordinates are defined"
name: space
- target_type: Image
quantity: 1
quantity: "?"
doc: "The reference image (e.g. mean or max projection) on which the coordinate map is based"
name: image
- target_type: ImageSeries
- target_type: ImagingPlane
quantity: "?"
doc: "The imaging series (OnePhotonSeries or TwoPhotonSeries) that this coordinate map applies to"
doc: "The imaging plane associated with the OnePhotonSeries or TwoPhotonSeries that this coordinate map applies to"
name: localized_entity
attributes:
- name: method
Expand Down Expand Up @@ -285,7 +282,6 @@ groups:
doc: 2D array of brain region names for each pixel
quantity: "?"


- neurodata_type_def: Landmarks
neurodata_type_inc: DynamicTable
doc: |
Expand Down Expand Up @@ -402,4 +398,4 @@ groups:
doc: "A spatial transformation used in the registration."
- neurodata_type_inc: Landmarks
quantity: "?"
doc: "Landmarks used in the registration."
doc: "Landmarks used in the registration."
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from hdmf.common import DynamicTable
from hdmf.utils import AllowPositional, get_docval
from pynwb.image import Image
from pynwb.ophys import OnePhotonSeries, TwoPhotonSeries
from pynwb.ophys import ImagingPlane

from pynwb import docval, get_class, register_class

Expand Down Expand Up @@ -363,6 +363,7 @@ def __init__(self, **kwargs):

@register_class("AnatomicalCoordinatesImage", "ndx-anatomical-localization")
class AnatomicalCoordinatesImage(TempAnatomicalCoordinatesImage):

@docval(
{"name": "name", "type": str, "doc": "name of the NWB object"},
{"name": "description", "type": str, "doc": "description of the NWB object", "default": None},
Expand All @@ -375,8 +376,10 @@ class AnatomicalCoordinatesImage(TempAnatomicalCoordinatesImage):
},
{
"name": "localized_entity",
"type": (OnePhotonSeries, TwoPhotonSeries),
"doc": "The imaging series (OnePhotonSeries or TwoPhotonSeries) that this coordinate map applies to",
"type": ImagingPlane,
"doc": (
"The imaging plane associated with the OnePhotonSeries or TwoPhotonSeries that this coordinate map applies to"
),
"default": None,
"allow_none": True,
},
Expand Down
112 changes: 16 additions & 96 deletions src/pynwb/tests/test_anatomical_coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pynwb.image import GrayscaleImage
from pynwb.testing.mock.ecephys import mock_ElectrodeTable
from pynwb.testing.mock.file import mock_NWBFile
from pynwb.testing.mock.ophys import mock_OnePhotonSeries, mock_TwoPhotonSeries
from pynwb.testing.mock.ophys import mock_ImagingPlane

from ndx_anatomical_localization import (
AffineTransformation,
Expand Down Expand Up @@ -153,15 +153,15 @@ def test_anatomical_coordinates_image_with_allen_ccfv3_space():
image_collection = nwbfile.processing["ophys"].data_interfaces["SummaryImages"]
image_collection.add_image(GrayscaleImage(name="MeanImage", data=np.ones((5, 5)), description="mean image"))

two_photon_series = mock_TwoPhotonSeries(nwbfile=nwbfile, name="TwoPhotonSeries")
imaging_plane = mock_ImagingPlane(nwbfile=nwbfile, name="MyImagingPlane")

space = AllenCCFv3Space()
localization.add_spaces([space])

image_coordinates = AnatomicalCoordinatesImage(
name="MyAnatomicalLocalization",
image=image_collection["MeanImage"],
localized_entity=two_photon_series,
localized_entity=imaging_plane,
method="method",
space=space,
x=np.ones((5, 5)),
Expand All @@ -178,14 +178,14 @@ def test_anatomical_coordinates_image_with_allen_ccfv3_space():
with NWBHDF5IO("test_image_ccf.nwb", "r", load_namespaces=True) as io:
read_nwbfile = io.read()
read_summary_image = read_nwbfile.processing["ophys"]["SummaryImages"]["MeanImage"]
read_two_photon_series = read_nwbfile.acquisition["TwoPhotonSeries"]
read_imaging_plane = read_nwbfile.imaging_planes["MyImagingPlane"]
read_localization = read_nwbfile.lab_meta_data["localization"]

read_coordinates_image = read_localization.anatomical_coordinates_images["MyAnatomicalLocalization"]

assert read_coordinates_image.method == "method"
assert read_coordinates_image.image is read_summary_image
assert read_coordinates_image.localized_entity is read_two_photon_series
assert read_coordinates_image.localized_entity is read_imaging_plane
assert isinstance(read_coordinates_image.space, AllenCCFv3Space)
npt.assert_array_equal(
read_coordinates_image.x[:],
Expand Down Expand Up @@ -465,7 +465,7 @@ def test_mebrains_space_write_read(tmp_path):
assert read_table.space.extent is None


def test_create_anatomical_coordinates_image_w_two_photon_series():
def test_create_anatomical_coordinates_image_w_imaging_plane():
nwbfile = mock_NWBFile()

localization = Localization()
Expand All @@ -477,7 +477,7 @@ def test_create_anatomical_coordinates_image_w_two_photon_series():
image_collection = nwbfile.processing["ophys"].data_interfaces["SummaryImages"]
image_collection.add_image(GrayscaleImage(name="MeanImage", data=np.ones((5, 5)), description="mean image"))

two_photon_series = mock_TwoPhotonSeries(nwbfile=nwbfile, name="TwoPhotonSeries")
imaging_plane = imaging_plane = mock_ImagingPlane(nwbfile=nwbfile, name="MyImagingPlane")

space = Space(
name="MySpace",
Expand All @@ -491,7 +491,7 @@ def test_create_anatomical_coordinates_image_w_two_photon_series():
image_coordinates = AnatomicalCoordinatesImage(
name="MyAnatomicalLocalization",
image=image_collection["MeanImage"],
localized_entity=two_photon_series,
localized_entity=imaging_plane,
method="method",
space=space,
x=np.ones((5, 5)),
Expand All @@ -508,14 +508,14 @@ def test_create_anatomical_coordinates_image_w_two_photon_series():
with NWBHDF5IO("test_image.nwb", "r", load_namespaces=True) as io:
read_nwbfile = io.read()
read_summary_image = read_nwbfile.processing["ophys"]["SummaryImages"]["MeanImage"]
read_two_photon_series = read_nwbfile.acquisition["TwoPhotonSeries"]
read_imaging_plane = read_nwbfile.imaging_planes["MyImagingPlane"]
read_localization = read_nwbfile.lab_meta_data["localization"]

read_coordinates_image = read_localization.anatomical_coordinates_images["MyAnatomicalLocalization"]

assert read_coordinates_image.method == "method"
assert read_coordinates_image.image is read_summary_image
assert read_coordinates_image.localized_entity is read_two_photon_series
assert read_coordinates_image.localized_entity is read_imaging_plane
assert read_coordinates_image.space.origin == "bregma"
npt.assert_array_equal(
read_coordinates_image.x[:],
Expand All @@ -532,56 +532,6 @@ def test_create_anatomical_coordinates_image_w_two_photon_series():
npt.assert_array_equal(read_coordinates_image.brain_region[:], np.array([["CA1"] * 5] * 5))


def test_create_anatomical_coordinates_image_w_one_photon_series():
nwbfile = mock_NWBFile()

localization = Localization()
nwbfile.add_lab_meta_data([localization])

if "ophys" not in nwbfile.processing:
nwbfile.create_processing_module("ophys", "ophys")
nwbfile.processing["ophys"].add(Images(name="SummaryImages", description="Summary images container"))
image_collection = nwbfile.processing["ophys"].data_interfaces["SummaryImages"]
image_collection.add_image(GrayscaleImage(name="MeanImage", data=np.ones((5, 5)), description="mean image"))

one_photon_series = mock_OnePhotonSeries(nwbfile=nwbfile, name="OnePhotonSeries")

space = Space(
name="MySpace",
space_name="MySpace",
origin="bregma",
units="um",
orientation="RAS",
)
localization.add_spaces([space])

image_coordinates = AnatomicalCoordinatesImage(
name="MyAnatomicalLocalization",
image=image_collection["MeanImage"],
localized_entity=one_photon_series,
method="method",
space=space,
x=np.ones((5, 5)),
y=np.ones((5, 5)) * 2.0,
z=np.ones((5, 5)) * 3.0,
)

localization.add_anatomical_coordinates_images([image_coordinates])

with NWBHDF5IO("test_image_ops.nwb", "w") as io:
io.write(nwbfile)

with NWBHDF5IO("test_image_ops.nwb", "r", load_namespaces=True) as io:
read_nwbfile = io.read()
read_one_photon_series = read_nwbfile.acquisition["OnePhotonSeries"]
read_localization = read_nwbfile.lab_meta_data["localization"]

read_coordinates_image = read_localization.anatomical_coordinates_images["MyAnatomicalLocalization"]

assert read_coordinates_image.method == "method"
assert read_coordinates_image.localized_entity is read_one_photon_series


def test_create_anatomical_coordinates_image_w_image():
nwbfile = mock_NWBFile()

Expand All @@ -595,7 +545,7 @@ def test_create_anatomical_coordinates_image_w_image():
image_collection = nwbfile.processing["ophys"].data_interfaces["SummaryImages"]
image_collection.add_image(GrayscaleImage(name="MyImage", data=np.ones((5, 5)), description="An example image"))

two_photon_series = mock_TwoPhotonSeries(nwbfile=nwbfile, name="TwoPhotonSeries")
imaging_plane = imaging_plane = mock_ImagingPlane(nwbfile=nwbfile, name="MyImagingPlane")

space = Space(
name="MySpace",
Expand All @@ -609,7 +559,7 @@ def test_create_anatomical_coordinates_image_w_image():
image_coordinates = AnatomicalCoordinatesImage(
name="MyAnatomicalLocalization",
image=image_collection["MyImage"],
localized_entity=two_photon_series,
localized_entity=imaging_plane,
method="method",
space=space,
x=np.ones((5, 5)),
Expand Down Expand Up @@ -648,36 +598,6 @@ def test_create_anatomical_coordinates_image_w_image():
npt.assert_array_equal(read_coordinates_image.brain_region[:], np.array([["CA1"] * 5] * 5))


def test_create_anatomical_coordinates_image_failing_missing_image():
"""
Missing required image should raise an error.
"""
nwbfile = mock_NWBFile()
localization = Localization()
nwbfile.add_lab_meta_data([localization])

space = Space(
name="MySpace",
space_name="MySpace",
origin="bregma",
units="um",
orientation="RAS",
)
localization.add_spaces([space])
two_photon_series = mock_TwoPhotonSeries(nwbfile=nwbfile, name="TwoPhotonSeries")

with pytest.raises(Exception):
AnatomicalCoordinatesImage(
name="MyAnatomicalLocalization",
localized_entity=two_photon_series,
method="method",
space=space,
x=np.ones((5, 5)),
y=np.ones((5, 5)) * 2.0,
z=np.ones((5, 5)) * 3.0,
)


def test_create_anatomical_coordinates_image_failing_shape_mismatch():
"""
Mismatched shape between image and x,y,z should raise ValueError
Expand All @@ -704,7 +624,7 @@ def test_create_anatomical_coordinates_image_failing_shape_mismatch():
image_collection = nwbfile.processing["ophys"].data_interfaces["SummaryImages"]
image_collection.add_image(GrayscaleImage(name="MyImage", data=np.ones((5, 5)), description="An example image"))

two_photon_series = mock_TwoPhotonSeries(nwbfile=nwbfile, name="TwoPhotonSeries")
imaging_plane = mock_ImagingPlane(nwbfile=nwbfile, name="MyImagingPlane")

x = np.ones((4, 5))
y = np.ones((5, 5)) * 2.0
Expand All @@ -713,7 +633,7 @@ def test_create_anatomical_coordinates_image_failing_shape_mismatch():
AnatomicalCoordinatesImage(
name="MyAnatomicalLocalization",
image=image_collection["MyImage"],
localized_entity=two_photon_series,
localized_entity=imaging_plane,
method="method",
space=space,
x=x,
Expand All @@ -735,7 +655,7 @@ def test_get_coordinates():
image_collection = nwbfile.processing["ophys"].data_interfaces["SummaryImages"]
image_collection.add_image(GrayscaleImage(name="MeanImage", data=np.ones((3, 3)), description="mean image"))

two_photon_series = mock_TwoPhotonSeries(nwbfile=nwbfile, name="TwoPhotonSeries")
imaging_plane = imaging_plane = mock_ImagingPlane(nwbfile=nwbfile, name="MyImagingPlane")

space = Space(
name="MySpace",
Expand All @@ -753,7 +673,7 @@ def test_get_coordinates():
coords = AnatomicalCoordinatesImage(
name="TestCoordinates",
image=image_collection["MeanImage"],
localized_entity=two_photon_series,
localized_entity=imaging_plane,
method="test_method",
space=space,
x=x_data,
Expand Down
Loading