Source code for pynwb.ophys

from collections.abc import Iterable
import numpy as np
import warnings

from hdmf.common import DynamicTable, DynamicTableRegion
from hdmf.utils import docval, popargs, get_docval, get_data_shape, popargs_to_dict

from . import register_class, CORE_NAMESPACE
from .base import TimeSeries
from .image import ImageSeries
from .core import NWBContainer, MultiContainerInterface, NWBDataInterface
from .device import Device


[docs] @register_class('OpticalChannel', CORE_NAMESPACE) class OpticalChannel(NWBContainer): """An optical channel used to record from an imaging plane.""" __nwbfields__ = ('description', 'emission_lambda') @docval({'name': 'name', 'type': str, 'doc': 'the name of this electrode'}, # required {'name': 'description', 'type': str, 'doc': 'Any notes or comments about the channel.'}, # required {'name': 'emission_lambda', 'type': float, 'doc': 'Emission wavelength for channel, in nm.'}) # required def __init__(self, **kwargs): description, emission_lambda = popargs("description", "emission_lambda", kwargs) super().__init__(**kwargs) self.description = description self.emission_lambda = emission_lambda
[docs] @register_class('ImagingPlane', CORE_NAMESPACE) class ImagingPlane(NWBContainer): """An imaging plane and its metadata.""" __nwbfields__ = ({'name': 'optical_channel', 'child': True}, 'description', 'device', 'excitation_lambda', 'imaging_rate', 'indicator', 'location', 'manifold', 'conversion', 'unit', 'reference_frame') @docval(*get_docval(NWBContainer.__init__, 'name'), # required {'name': 'optical_channel', 'type': (list, OpticalChannel), # required 'doc': 'One of possibly many groups storing channel-specific data.'}, {'name': 'description', 'type': str, 'doc': 'Description of this ImagingPlane.'}, # required {'name': 'device', 'type': Device, 'doc': 'the device that was used to record'}, # required {'name': 'excitation_lambda', 'type': float, 'doc': 'Excitation wavelength in nm.'}, # required {'name': 'indicator', 'type': str, 'doc': 'Calcium indicator'}, # required {'name': 'location', 'type': str, 'doc': 'Location of image plane.'}, # required {'name': 'imaging_rate', 'type': float, 'doc': 'Rate images are acquired, in Hz. If the corresponding TimeSeries is present, the rate should be ' 'stored there instead.', 'default': None}, {'name': 'manifold', 'type': 'array_data', 'doc': ('DEPRECATED - Physical position of each pixel. size=("height", "width", "xyz"). ' 'Deprecated in favor of origin_coords and grid_spacing.'), 'default': None}, {'name': 'conversion', 'type': float, 'doc': ('DEPRECATED - Multiplier to get from stored values to specified unit (e.g., 1e-3 for millimeters) ' 'Deprecated in favor of origin_coords and grid_spacing.'), 'default': 1.0}, {'name': 'unit', 'type': str, 'doc': 'DEPRECATED - Base unit that coordinates are stored in (e.g., Meters). ' 'Deprecated in favor of origin_coords_unit and grid_spacing_unit.', 'default': 'meters'}, {'name': 'reference_frame', 'type': str, 'doc': 'Describes position and reference frame of manifold based on position of first element ' 'in manifold.', 'default': None}, {'name': 'origin_coords', 'type': 'array_data', 'doc': 'Physical location of the first element of the imaging plane (0, 0) for 2-D data or (0, 0, 0) for ' '3-D data. See also reference_frame for what the physical location is relative to (e.g., bregma).', 'default': None}, {'name': 'origin_coords_unit', 'type': str, 'doc': "Measurement units for origin_coords. The default value is 'meters'.", 'default': 'meters'}, {'name': 'grid_spacing', 'type': 'array_data', 'doc': "Space between pixels in (x, y) or voxels in (x, y, z) directions, in the specified unit. Assumes " "imaging plane is a regular grid. See also reference_frame to interpret the grid.", 'default': None}, {'name': 'grid_spacing_unit', 'type': str, 'doc': "Measurement units for grid_spacing. The default value is 'meters'.", 'default': 'meters'}) def __init__(self, **kwargs): keys_to_set = ('optical_channel', 'description', 'device', 'excitation_lambda', 'imaging_rate', 'indicator', 'location', 'manifold', 'conversion', 'unit', 'reference_frame', 'origin_coords', 'origin_coords_unit', 'grid_spacing', 'grid_spacing_unit') args_to_set = popargs_to_dict(keys_to_set, kwargs) super().__init__(**kwargs) if not isinstance(args_to_set['optical_channel'], list): args_to_set['optical_channel'] = [args_to_set['optical_channel']] if args_to_set['manifold'] is not None: warnings.warn("The 'manifold' argument is deprecated in favor of 'origin_coords' and 'grid_spacing'.", DeprecationWarning) if args_to_set['conversion'] != 1.0: warnings.warn("The 'conversion' argument is deprecated in favor of 'origin_coords' and 'grid_spacing'.", DeprecationWarning) if args_to_set['unit'] != 'meters': warnings.warn("The 'unit' argument is deprecated in favor of 'origin_coords_unit' and 'grid_spacing_unit'.", DeprecationWarning) for key, val in args_to_set.items(): setattr(self, key, val)
[docs] @register_class("OnePhotonSeries", CORE_NAMESPACE) class OnePhotonSeries(ImageSeries): """Image stack recorded over time from 1-photon microscope.""" __nwbfields__ = ( "imaging_plane", "pmt_gain", "scan_line_rate", "exposure_time", "binning", "power", "intensity" ) @docval( *get_docval(ImageSeries.__init__, "name"), # required {"name": "imaging_plane", "type": ImagingPlane, "doc": "Imaging plane class/pointer."}, # required *get_docval(ImageSeries.__init__, "data", "unit", "format"), {"name": "pmt_gain", "type": float, "doc": "Photomultiplier gain.", "default": None}, { "name": "scan_line_rate", "type": float, "doc": ( "Lines imaged per second. This is also stored in /general/optophysiology but is kept " "here as it is useful information for analysis, and so good to be stored w/ the actual data." ), "default": None, }, { "name": "exposure_time", "type": float, "doc": "Exposure time of the sample; often the inverse of the frequency.", "default": None, }, { "name": "binning", "type": (int, "uint"), "doc": "Amount of pixels combined into 'bins'; could be 1, 2, 4, 8, etc.", "default": None, }, { "name": "power", "type": float, "doc": "Power of the excitation in mW, if known.", "default": None, }, { "name": "intensity", "type": float, "doc": "Intensity of the excitation in mW/mm^2, if known.", "default": None, }, *get_docval( ImageSeries.__init__, "external_file", "starting_frame", "bits_per_pixel", "dimension", "resolution", "conversion", "timestamps", "starting_time", "rate", "comments", "description", "control", "control_description", "device", "offset", ) ) def __init__(self, **kwargs): keys_to_set = ( "imaging_plane", "pmt_gain", "scan_line_rate", "exposure_time", "binning", "power", "intensity" ) args_to_set = popargs_to_dict(keys_to_set, kwargs) super().__init__(**kwargs) if args_to_set["binning"] is not None and args_to_set["binning"] < 0: raise ValueError(f"Binning value must be >= 0: {args_to_set['binning']}") if isinstance(args_to_set["binning"], int): args_to_set["binning"] = np.uint(args_to_set["binning"]) for key, val in args_to_set.items(): setattr(self, key, val)
[docs] @register_class('TwoPhotonSeries', CORE_NAMESPACE) class TwoPhotonSeries(ImageSeries): """Image stack recorded over time from 2-photon microscope.""" __nwbfields__ = ('field_of_view', 'imaging_plane', 'pmt_gain', 'scan_line_rate') @docval(*get_docval(ImageSeries.__init__, 'name'), # required {'name': 'imaging_plane', 'type': ImagingPlane, 'doc': 'Imaging plane class/pointer.'}, # required *get_docval(ImageSeries.__init__, 'data', 'unit', 'format'), {'name': 'field_of_view', 'type': (Iterable, TimeSeries), 'shape': ((2, ), (3, )), 'doc': 'Width, height and depth of image, or imaged area (meters).', 'default': None}, {'name': 'pmt_gain', 'type': float, 'doc': 'Photomultiplier gain.', 'default': None}, {'name': 'scan_line_rate', 'type': float, 'doc': 'Lines imaged per second. This is also stored in /general/optophysiology but is kept ' 'here as it is useful information for analysis, and so good to be stored w/ the actual data.', 'default': None}, *get_docval(ImageSeries.__init__, 'external_file', 'starting_frame', 'bits_per_pixel', 'dimension', 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments', 'description', 'control', 'control_description', 'device', 'offset')) def __init__(self, **kwargs): keys_to_set = ("field_of_view", "imaging_plane", "pmt_gain", "scan_line_rate") args_to_set = popargs_to_dict(keys_to_set, kwargs) super().__init__(**kwargs) for key, val in args_to_set.items(): setattr(self, key, val)
[docs] @register_class('CorrectedImageStack', CORE_NAMESPACE) class CorrectedImageStack(NWBDataInterface): """ An image stack where all frames are shifted (registered) to a common coordinate system, to account for movement and drift between frames. Note: each frame at each point in time is assumed to be 2-D (has only x & y dimensions). """ __nwbfields__ = ({'name': 'corrected', 'child': True, 'required_name': 'corrected'}, {'name': 'xy_translation', 'child': True, 'required_name': 'xy_translation'}, 'original') @docval({'name': 'name', 'type': str, 'doc': 'The name of this CorrectedImageStack container', 'default': 'CorrectedImageStack'}, {'name': 'corrected', 'type': ImageSeries, 'doc': 'Image stack with frames shifted to the common coordinates. This must have the name "corrected".'}, {'name': 'original', 'type': ImageSeries, 'doc': 'Link to image series that is being registered.'}, {'name': 'xy_translation', 'type': TimeSeries, 'doc': 'Stores the x,y delta necessary to align each frame to the common coordinates, ' 'for example, to align each frame to a reference image. This must have the name "xy_translation".'}) def __init__(self, **kwargs): corrected, original, xy_translation = popargs('corrected', 'original', 'xy_translation', kwargs) super().__init__(**kwargs) self.corrected = corrected self.original = original self.xy_translation = xy_translation
[docs] @register_class('MotionCorrection', CORE_NAMESPACE) class MotionCorrection(MultiContainerInterface): """ A collection of corrected images stacks. """ __clsconf__ = { 'add': 'add_corrected_image_stack', 'get': 'get_corrected_image_stack', 'create': 'create_corrected_image_stack', 'type': CorrectedImageStack, 'attr': 'corrected_image_stacks' }
[docs] @register_class('PlaneSegmentation', CORE_NAMESPACE) class PlaneSegmentation(DynamicTable): """ Stores pixels in an image that represent different regions of interest (ROIs) or masks. All segmentation for a given imaging plane is stored together, with storage for multiple imaging planes (masks) supported. Each ROI is stored in its own subgroup, with the ROI group containing both a 2D mask and a list of pixels that make up this mask. Segments can also be used for masking neuropil. If segmentation is allowed to change with time, a new imaging plane (or module) is required and ROI names should remain consistent between them. """ __fields__ = ('imaging_plane', {'name': 'reference_images', 'child': True}) __columns__ = ( {'name': 'image_mask', 'description': 'Image masks for each ROI'}, {'name': 'pixel_mask', 'description': 'Pixel masks for each ROI', 'index': True}, {'name': 'voxel_mask', 'description': 'Voxel masks for each ROI', 'index': True} ) @docval({'name': 'description', 'type': str, # required 'doc': 'Description of image plane, recording wavelength, depth, etc.'}, {'name': 'imaging_plane', 'type': ImagingPlane, # required 'doc': 'the ImagingPlane this ROI applies to'}, {'name': 'name', 'type': str, 'doc': 'name of PlaneSegmentation.', 'default': None}, {'name': 'reference_images', 'type': (ImageSeries, list, dict, tuple), 'default': None, 'doc': 'One or more image stacks that the masks apply to (can be oneelement stack).'}, *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames')) def __init__(self, **kwargs): imaging_plane, reference_images = popargs('imaging_plane', 'reference_images', kwargs) if kwargs['name'] is None: kwargs['name'] = imaging_plane.name super().__init__(**kwargs) self.imaging_plane = imaging_plane if isinstance(reference_images, ImageSeries): reference_images = (reference_images,) self.reference_images = reference_images
[docs] @docval({'name': 'pixel_mask', 'type': 'array_data', 'default': None, 'doc': 'pixel mask for 2D ROIs: [(x1, y1, weight1), (x2, y2, weight2), ...]', 'shape': (None, 3)}, {'name': 'voxel_mask', 'type': 'array_data', 'default': None, 'doc': 'voxel mask for 3D ROIs: [(x1, y1, z1, weight1), (x2, y2, z2, weight2), ...]', 'shape': (None, 4)}, {'name': 'image_mask', 'type': 'array_data', 'default': None, 'doc': 'image with the same size of image where positive values mark this ROI', 'shape': [[None]*2, [None]*3]}, {'name': 'id', 'type': int, 'doc': 'the ID for the ROI', 'default': None}, allow_extra=True) def add_roi(self, **kwargs): """Add a Region Of Interest (ROI) data to this""" pixel_mask, voxel_mask, image_mask = popargs('pixel_mask', 'voxel_mask', 'image_mask', kwargs) if image_mask is None and pixel_mask is None and voxel_mask is None: raise ValueError("Must provide 'image_mask' and/or 'pixel_mask'") rkwargs = dict(kwargs) if image_mask is not None: rkwargs['image_mask'] = image_mask if pixel_mask is not None: rkwargs['pixel_mask'] = pixel_mask if voxel_mask is not None: rkwargs['voxel_mask'] = voxel_mask return super().add_row(**rkwargs)
[docs] @staticmethod def pixel_to_image(pixel_mask): """Converts a 2D pixel_mask of a ROI into an image_mask.""" image_matrix = np.zeros(np.shape(pixel_mask)) npmask = np.asarray(pixel_mask) x_coords = npmask[:, 0].astype(np.int32) y_coords = npmask[:, 1].astype(np.int32) weights = npmask[:, -1] image_matrix[y_coords, x_coords] = weights return image_matrix
[docs] @staticmethod def image_to_pixel(image_mask): """Converts an image_mask of a ROI into a pixel_mask""" pixel_mask = [] it = np.nditer(image_mask, flags=['multi_index']) while not it.finished: weight = it[0][()] if weight > 0: x = it.multi_index[0] y = it.multi_index[1] pixel_mask.append([x, y, weight]) it.iternext() return pixel_mask
[docs] @docval({'name': 'description', 'type': str, 'doc': 'a brief description of what the region is'}, {'name': 'region', 'type': (slice, list, tuple), 'doc': 'the indices of the table', 'default': slice(None)}, {'name': 'name', 'type': str, 'doc': 'the name of the ROITableRegion', 'default': 'rois'}) def create_roi_table_region(self, **kwargs): return self.create_region(**kwargs)
[docs] @register_class('ImageSegmentation', CORE_NAMESPACE) class ImageSegmentation(MultiContainerInterface): """ Stores pixels in an image that represent different regions of interest (ROIs) or masks. All segmentation for a given imaging plane is stored together, with storage for multiple imaging planes (masks) supported. Each ROI is stored in its own subgroup, with the ROI group containing both a 2D mask and a list of pixels that make up this mask. Segments can also be used for masking neuropil. If segmentation is allowed to change with time, a new imaging plane (or module) is required and ROI names should remain consistent between them. """ __clsconf__ = { 'attr': 'plane_segmentations', 'type': PlaneSegmentation, 'add': 'add_plane_segmentation', 'get': 'get_plane_segmentation', 'create': 'create_plane_segmentation' }
[docs] @docval({'name': 'imaging_plane', 'type': ImagingPlane, 'doc': 'the ImagingPlane this ROI applies to'}, {'name': 'description', 'type': str, 'doc': 'Description of image plane, recording wavelength, depth, etc.', 'default': None}, {'name': 'name', 'type': str, 'doc': 'name of PlaneSegmentation.', 'default': None}) def add_segmentation(self, **kwargs): kwargs.setdefault('description', kwargs['imaging_plane'].description) return self.create_plane_segmentation(**kwargs)
[docs] @register_class('RoiResponseSeries', CORE_NAMESPACE) class RoiResponseSeries(TimeSeries): ''' ROI responses over an imaging plane. Each column in data should correspond to the signal from one ROI. ''' __nwbfields__ = ({'name': 'rois', 'child': True},) @docval(*get_docval(TimeSeries.__init__, 'name'), # required {'name': 'data', 'type': ('array_data', 'data', TimeSeries), # required 'shape': ((None, ), (None, None)), 'doc': ('The data values. May be 1D or 2D. The first dimension must be time. The optional second ' 'dimension represents ROIs')}, *get_docval(TimeSeries.__init__, 'unit'), {'name': 'rois', 'type': DynamicTableRegion, # required 'doc': 'a table region corresponding to the ROIs that were used to generate this data'}, *get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments', 'description', 'control', 'control_description', 'offset')) def __init__(self, **kwargs): rois = popargs('rois', kwargs) data_shape = get_data_shape(data=kwargs["data"], strict_no_data_load=True) rois_shape = get_data_shape(data=rois.data, strict_no_data_load=True) if ( data_shape is not None and rois_shape is not None # check that data is 2d and rois is 1d and len(data_shape) == 2 and len(rois_shape) == 1 # check that key dimensions are known and data_shape[1] is not None and rois_shape[0] is not None and data_shape[1] != rois_shape[0] ): if data_shape[0] == rois_shape[0]: warnings.warn("%s '%s': The second dimension of data does not match the length of rois, " "but instead the first does. Data is oriented incorrectly and should be transposed." % (self.__class__.__name__, kwargs["name"])) else: warnings.warn("%s '%s': The second dimension of data does not match the length of rois. " "Your data may be transposed." % (self.__class__.__name__, kwargs["name"])) super().__init__(**kwargs) self.rois = rois
[docs] @register_class('DfOverF', CORE_NAMESPACE) class DfOverF(MultiContainerInterface): """ dF/F information about a region of interest (ROI). Storage hierarchy of dF/F should be the same as for segmentation (ie, same names for ROIs and for image planes). """ __clsconf__ = { 'attr': 'roi_response_series', 'type': RoiResponseSeries, 'add': 'add_roi_response_series', 'get': 'get_roi_response_series', 'create': 'create_roi_response_series' }
[docs] @register_class('Fluorescence', CORE_NAMESPACE) class Fluorescence(MultiContainerInterface): """ Fluorescence information about a region of interest (ROI). Storage hierarchy of fluorescence should be the same as for segmentation (ie, same names for ROIs and for image planes). """ __clsconf__ = { 'attr': 'roi_response_series', 'type': RoiResponseSeries, 'add': 'add_roi_response_series', 'get': 'get_roi_response_series', 'create': 'create_roi_response_series' }