How to Annotate Optical Physiology Data#

This guide provides instructions for annotating optical physiology (ophys) data using NeuroConv.

Optical physiology metadata in NWB files includes information about the imaging device (microscope), imaging planes (where and how the imaging was performed), microscopy series (the actual imaging data), and segmentation results (ROIs and response traces).

How to Annotate a Single Data Interface#

Each imaging interface uses a metadata_key parameter that organizes all related metadata components. The same key is used to access the ImagingPlane, MicroscopySeries, and Device - keeping them linked together.

from neuroconv.datainterfaces import TiffImagingInterface

# Define a metadata_key that will link all components together
metadata_key = "visual_cortex"

# Create an interface with the metadata_key
interface = TiffImagingInterface(
    file_path="path/to/imaging_data.tif",
    sampling_frequency=30.0,
    metadata_key=metadata_key,
)

metadata = interface.get_metadata()

# The same metadata_key is used to access all related components:
# - metadata["Devices"][metadata_key] -> the imaging device
# - metadata["Ophys"]["ImagingPlanes"][metadata_key] -> the imaging plane
# - metadata["Ophys"]["MicroscopySeries"][metadata_key] -> the microscopy series

# Annotate the imaging plane
imaging_plane = metadata["Ophys"]["ImagingPlanes"][metadata_key]
imaging_plane["name"] = "ImagingPlaneVisualCortex"
imaging_plane["description"] = "Imaging plane in V1 layer 2/3"
imaging_plane["indicator"] = "GCaMP6s"
imaging_plane["location"] = "V1 layer 2/3"
imaging_plane["excitation_lambda"] = 920.0
imaging_plane["optical_channel"][0]["emission_lambda"] = 510.0

# Annotate the microscopy series
microscopy_series = metadata["Ophys"]["MicroscopySeries"][metadata_key]
microscopy_series["name"] = "TwoPhotonSeriesVisualCortex"
microscopy_series["description"] = "Calcium imaging during visual stimulation"

# Annotate the device
device = metadata["Devices"][metadata_key]
device["name"] = "Microscope"
device["description"] = "Custom two-photon microscope, data acquired with ScanImage (VIDRIO)"
device["manufacturer"] = "DIY"

# Convert to NWB - specify the series type (TwoPhotonSeries or OnePhotonSeries)
nwbfile = interface.create_nwbfile(
    metadata=metadata,
    photon_series_type="TwoPhotonSeries",  # Choose the NWB neurodata type
)

How to Annotate Multi-Plane Imaging Data#

When you have imaging data from multiple planes (e.g., imaging different cortical layers), use a different metadata_key for each plane. This creates separate, properly linked components for each imaging plane.

from neuroconv.datainterfaces import TiffImagingInterface
from neuroconv import NWBConverter

# Define a metadata_key for each cortical layer
layer2_3_metadata_key = "layer2_3"
layer4_metadata_key = "layer4"
layer5_metadata_key = "layer5"

# Create an interface for each cortical layer with its own metadata_key
layer2_3_interface = TiffImagingInterface(
    file_path="path/to/layer2_3.tif",
    sampling_frequency=30.0,
    metadata_key=layer2_3_metadata_key,
)

layer4_interface = TiffImagingInterface(
    file_path="path/to/layer4.tif",
    sampling_frequency=30.0,
    metadata_key=layer4_metadata_key,
)

layer5_interface = TiffImagingInterface(
    file_path="path/to/layer5.tif",
    sampling_frequency=30.0,
    metadata_key=layer5_metadata_key,
)

# Combine all planes in a converter
converter = NWBConverter(
    data_interfaces={
        "layer2_3_interface": layer2_3_interface,
        "layer4_interface": layer4_interface,
        "layer5_interface": layer5_interface,
    }
)

metadata = converter.get_metadata()

# Annotate each plane with its cortical layer information
metadata["Ophys"]["ImagingPlanes"][layer2_3_metadata_key]["name"] = "ImagingPlaneLayer2_3"
metadata["Ophys"]["ImagingPlanes"][layer2_3_metadata_key]["description"] = "V1 layer 2/3 at 150um depth"
metadata["Ophys"]["ImagingPlanes"][layer2_3_metadata_key]["location"] = "V1 layer 2/3"
metadata["Ophys"]["ImagingPlanes"][layer4_metadata_key]["name"] = "ImagingPlaneLayer4"
metadata["Ophys"]["ImagingPlanes"][layer4_metadata_key]["description"] = "V1 layer 4 at 350um depth"
metadata["Ophys"]["ImagingPlanes"][layer4_metadata_key]["location"] = "V1 layer 4"
metadata["Ophys"]["ImagingPlanes"][layer5_metadata_key]["name"] = "ImagingPlaneLayer5"
metadata["Ophys"]["ImagingPlanes"][layer5_metadata_key]["description"] = "V1 layer 5 at 500um depth"
metadata["Ophys"]["ImagingPlanes"][layer5_metadata_key]["location"] = "V1 layer 5"

# Set common metadata across all planes
for layer_key in [layer2_3_metadata_key, layer4_metadata_key, layer5_metadata_key]:
    metadata["Ophys"]["ImagingPlanes"][layer_key]["indicator"] = "GCaMP6s"
    metadata["Ophys"]["ImagingPlanes"][layer_key]["excitation_lambda"] = 920.0

# Specify photon_series_type in conversion_options for each interface
converter.run_conversion(
    nwbfile_path="multiplane_imaging.nwb",
    metadata=metadata,
    conversion_options={
        "layer2_3_interface": {"photon_series_type": "TwoPhotonSeries"},
        "layer4_interface": {"photon_series_type": "TwoPhotonSeries"},
        "layer5_interface": {"photon_series_type": "TwoPhotonSeries"},
    }
)

How to Annotate Multiple Segmentations of the Same Data#

When you run multiple segmentation pipelines on the same imaging data (e.g., comparing Suite2p and CaImAn), use a different metadata_key for each pipeline. Link them to the same imaging plane using imaging_plane_metadata_key to indicate they are segmenting the same data.

from neuroconv.datainterfaces import Suite2pSegmentationInterface, CaimanSegmentationInterface
from neuroconv import NWBConverter

# Each segmentation pipeline gets its own metadata_key
suite2p_metadata_key = "suite2p_metadata_key"
caiman_metadata_key = "caiman_metadata_key"

suite2p_segmentation = Suite2pSegmentationInterface(
    folder_path="path/to/suite2p/plane0",
    metadata_key=suite2p_metadata_key,
)

caiman_segmentation = CaimanSegmentationInterface(
    file_path="path/to/caiman_results.hdf5",
    metadata_key=caiman_metadata_key,
)

converter = NWBConverter(
    data_interfaces={
        "suite2p_interface": suite2p_segmentation,
        "caiman_interface": caiman_segmentation,
    }
)

metadata = converter.get_metadata()

# Define the device used for imaging
device_metadata_key = "my_device"
metadata["Devices"][device_metadata_key] = {
    "name": "Microscope",
    "description": "Two-photon microscope",
    "manufacturer": "Thorlabs",
}

# Define the imaging plane that both segmentations are derived from
imaging_plane_metadata_key = "my_imaging_plane"
metadata["Ophys"]["ImagingPlanes"][imaging_plane_metadata_key] = {
    "name": "ImagingPlane",
    "description": "Imaging plane in V1",
    "indicator": "GCaMP6s",
    "location": "V1 layer 2/3",
    "excitation_lambda": 920.0,
    "device_metadata_key": device_metadata_key,
}

# Annotate each pipeline's segmentation
metadata["Ophys"]["PlaneSegmentations"][suite2p_metadata_key]["name"] = "PlaneSegmentationSuite2p"
metadata["Ophys"]["PlaneSegmentations"][suite2p_metadata_key]["description"] = "Suite2p ROI detection"
metadata["Ophys"]["PlaneSegmentations"][caiman_metadata_key]["name"] = "PlaneSegmentationCaImAn"
metadata["Ophys"]["PlaneSegmentations"][caiman_metadata_key]["description"] = "CaImAn CNMF-E ROI detection"

# Link both segmentations to the same imaging plane
metadata["Ophys"]["PlaneSegmentations"][suite2p_metadata_key]["imaging_plane_metadata_key"] = imaging_plane_metadata_key
metadata["Ophys"]["PlaneSegmentations"][caiman_metadata_key]["imaging_plane_metadata_key"] = imaging_plane_metadata_key

converter.run_conversion(
    nwbfile_path="multi_pipeline_segmentation.nwb",
    metadata=metadata,
)

How to Annotate from a Template#

The sections above reach into get_metadata() one entry at a time, so that each block and each link between them is visible. Once you know the shape, you do not have to type it again. get_metadata_template() returns the whole thing already assembled, sized to what this interface writes, with every cross-reference resolved and every field only you can answer set to None:

from neuroconv.datainterfaces import TiffImagingInterface

interface = TiffImagingInterface(
    file_path="path/to/imaging_data.tif",
    sampling_frequency=30.0,
    metadata_key="visual_cortex",
)
metadata = interface.get_metadata_template()

What comes back is printed in full, in both YAML and JSON, at Metadata Templates.

The blanks are the checklist. What comes back None is exactly what the source could not tell us, and everything else is already done: the device_metadata_key that links the plane to the microscope and the imaging_plane_metadata_key that links the series to the plane are wired for you. Fill in what applies and delete what does not, since a blank left behind is refused at write time rather than guessed at.

It also shows what it does not require. The device model, the plane’s coordinates and the scanner fields are optional, and they appear so that you know the writer accepts them at all; drop the ones this recording cannot answer. Every entry’s name comes back blank, so keeping one costs naming it, which is what stops an offered entry reaching the file because nobody looked at it. The keys are handles rather than names in the file, so rename them freely, except the trace and image names under RoiResponses and SegmentationImages, which are roiextractors’ own.

imaging_plane = metadata["Ophys"]["ImagingPlanes"]["visual_cortex"]
imaging_plane["name"] = "ImagingPlaneVisualCortex"
imaging_plane["description"] = "Imaging plane in V1 layer 2/3"
imaging_plane["excitation_lambda"] = 920.0
imaging_plane["indicator"] = "GCaMP6s"
imaging_plane["location"] = "V1 layer 2/3"
imaging_plane["imaging_rate"] = 30.0
optical_channel = imaging_plane["optical_channel"][0]
optical_channel["name"] = "Green"
optical_channel["description"] = "Green channel"
optical_channel["emission_lambda"] = 510.0

microscopy_series = metadata["Ophys"]["MicroscopySeries"]["visual_cortex"]
microscopy_series["name"] = "TwoPhotonSeriesVisualCortex"
microscopy_series["description"] = "Calcium imaging during visual stimulation"
microscopy_series["unit"] = "n.a."

device = metadata["Devices"]["microscope"]
device["name"] = "Microscope"
device["description"] = "Custom two-photon microscope"
device["serial_number"] = "2019-04-01"

device_model = metadata["DeviceModels"]["microscope_model"]
device_model["name"] = "MicroscopeModel"
device_model["manufacturer"] = "DIY"
device_model["description"] = "Custom-built, no catalog model"

# This rig did not measure where the plane sat, and the scanner settings were not recorded.
del imaging_plane["origin_coords"]
del imaging_plane["origin_coords_unit"]
del imaging_plane["grid_spacing"]
del imaging_plane["grid_spacing_unit"]
del imaging_plane["reference_frame"]
del microscopy_series["field_of_view"]
del microscopy_series["pmt_gain"]
del microscopy_series["scan_line_rate"]
del metadata["DeviceModels"]["microscope_model"]["model_number"]

nwbfile = interface.create_nwbfile(metadata=metadata)

A segmentation interface offers the same thing, sized to what its pipeline produced. Only the traces and the summary images the file actually holds appear, so the entries you get back are the list to fill in rather than a menu to check against your output:

from neuroconv.datainterfaces import Suite2pSegmentationInterface

interface = Suite2pSegmentationInterface(
    folder_path="path/to/suite2p/plane0",
    metadata_key="suite2p",
)
metadata = interface.get_metadata_template()

plane_segmentation = metadata["Ophys"]["PlaneSegmentations"]["suite2p"]
plane_segmentation["name"] = "PlaneSegmentationSuite2p"
plane_segmentation["description"] = "ROIs detected by Suite2p"

# One entry per trace this run produced. A run that computed df/F has a "dff" entry here too.
roi_responses = metadata["Ophys"]["RoiResponses"]["suite2p"]
roi_responses["raw"]["name"] = "RoiResponseSeries"
roi_responses["raw"]["description"] = "Raw fluorescence traces from Suite2p"
roi_responses["raw"]["unit"] = "n.a."
roi_responses["neuropil"]["name"] = "Neuropil"
roi_responses["neuropil"]["description"] = "Neuropil fluorescence traces from Suite2p"
roi_responses["neuropil"]["unit"] = "n.a."
roi_responses["deconvolved"]["name"] = "Deconvolved"
roi_responses["deconvolved"]["description"] = "Deconvolved activity from Suite2p"
roi_responses["deconvolved"]["unit"] = "n.a."

segmentation_images = metadata["Ophys"]["SegmentationImages"]["suite2p"]
segmentation_images["mean"]["name"] = "mean_image"
segmentation_images["mean"]["description"] = "Mean image from Suite2p"
segmentation_images["correlation"]["name"] = "correlation_image"
segmentation_images["correlation"]["description"] = "Correlation image from Suite2p"

The imaging plane and the device come back on the segmentation template too, and are filled in exactly as above. If the same conversion also writes the imaging the pipeline ran on, do not fill them in twice: point the segmentation’s imaging_plane_metadata_key at the imaging interface’s plane, as the sections above do, and delete the plane the segmentation template offered.

Note

If you have a use case not covered here, please open an issue at NeuroConv GitHub Issues.